Tutorial: Inicio de sesión de los usuarios y llamada a Microsoft Graph API desde una aplicación de Android

En este tutorial creará una aplicación Android que se integrará con la plataforma de identidad de Microsoft Entra ID para que los usuarios inicien sesión y obtengan un token de acceso para llamar a Microsoft Graph API.

Cuando haya completado este tutorial, la aplicación aceptará inicios de sesión de cuentas personales de Microsoft (incluidas Outlook.com, live.com y otras) y cuentas profesionales o educativas de cualquier empresa u organización que use Microsoft Entra ID.

En este tutorial:

  • Creación de un proyecto de aplicación de Android en Android Studio
  • Registro de la aplicación en el Centro de administración de Microsoft Entra
  • Agregar código para admitir el inicio y el cierre de sesión de usuario
  • Agregar código para llamar a Microsoft Graph API
  • Prueba de la aplicación

Prerrequisitos

Funcionamiento de este tutorial

Screenshot of how the sample app generated by this tutorial works.

La aplicación de este tutorial iniciará la sesión de los usuarios y obtendrá datos en su nombre. A estos datos se accederá a través de una API protegida (Microsoft Graph API) que requiere autorización y que está protegida por la plataforma de identidad de Microsoft.

Esta muestra usa la biblioteca de autenticación de Microsoft (MSAL) para Android para implementar la autenticación: com.microsoft.identity.client.

Crear un proyecto

Sigue estos pasos para crear un nuevo proyecto si aún no tienes una aplicación de Android.

  1. Abra Android Studio y seleccione Start a new Android Studio project (Iniciar un nuevo proyecto de Android Studio).
  2. Seleccione Basic Activity (Actividad básica) y, después, seleccione Siguiente.
  3. Ponle un nombre a la aplicación, como MSALAndroidapp.
  4. Registra el nombre del paquete que se usará en pasos posteriores.
  5. Cambie el lenguaje de Kotlin a Java.
  6. Establezca el nivel de API de SDK mínimo en API 16 o superior y selecciona Finalizar.

Registro de la aplicación con Microsoft Entra ID

Sugerencia

Los pasos de este artículo pueden variar ligeramente en función del portal desde donde comienza.

  1. Inicie sesión en el Centro de administración de Microsoft Entra al menos como Desarrollador de aplicaciones.

  2. Si tiene acceso a varios inquilinos, use el icono Configuración del menú superior para cambiar al inquilino en el que desea registrar la aplicación desde el menú Directorios y suscripciones.

  3. Vaya aIdentidad>Aplicaciones>Registros de aplicaciones.

  4. Seleccione Nuevo registro.

  5. Escriba el nombre de la aplicación. Los usuarios de la aplicación pueden ver este nombre, el cual se puede cambiar más tarde.

  6. En Tipos de cuenta admitidos, seleccione Cuentas en cualquier directorio organizativo (cualquier directorio de Microsoft Entra: multiinquilino) y cuentas de Microsoft personales (como Skype o Xbox). Para obtener información sobre los distintos tipos de cuenta, selecciona la opción Ayudarme a elegir.

  7. Seleccione Registrar.

  8. En Administrar, seleccione Autenticación>Agregar una plataforma>Android.

  9. Escriba el nombre del paquete del proyecto. Si descargó el código de ejemplo, este valor es com.azuresamples.msalandroidapp.

  10. En la sección Hash de firma del panel Configurar la aplicación para Android , selecciona Se está generando un hash de firma de desarrollo y copia el comando KeyTool que quieres que se use para la plataforma.

  11. Escriba el hash de firma generado por KeyTool.

  12. Selecciona Configurar y guarda la configuración de MSAL que aparece en el panel Configuración de Android para que puedas especificarla cuando configures la aplicación más tarde.

  13. Seleccione Listo.

Configuración de la aplicación

  1. En el panel del proyecto de Android Studio, vaya a app\src\main\res.

  2. Haga clic con el botón derecho en res y elija New>Directory (Nuevo > Directorio). Escribe raw como el nombre del nuevo directorio y selecciona Aceptar.

  3. En app>src>main>res>raw, cree un nuevo archivo JSON llamado auth_config_single_account.json y pegue la configuración de MSAL que guardó anteriormente.

    Debajo del identificador URI de redirección, pegue:

      "account_mode" : "SINGLE",
    

    El archivo de configuración debe ser similar a este ejemplo:

    {
      "client_id": "00001111-aaaa-bbbb-3333-cccc4444",
      "authorization_user_agent": "WEBVIEW",
      "redirect_uri": "msauth://com.azuresamples.msalandroidapp/00001111%cccc4444%3D",
      "broker_redirect_uri_registered": true,
      "account_mode": "SINGLE",
      "authorities": [
        {
          "type": "AAD",
          "audience": {
            "type": "AzureADandPersonalMicrosoftAccount",
            "tenant_id": "common"
          }
        }
      ]
    }
    

    Como este tutorial solo muestra cómo configurar una aplicación en el modo de cuenta única, consulta el modo de cuenta única o múltiple y la configurar tu aplicación para obtener más información

  4. Se recomienda usar "WEBVIEW". En caso de que desee configurar "authorization_user_agent" como "BROWSER" en la aplicación, debe realizar las siguientes actualizaciones. a) Actualice auth_config_single_account.json con "authorization_user_agent": "Browser". b) Actualice AndroidManifest.xml. En la aplicación, vaya aapp>src>main>AndroidManifest.xml agregue la actividad BrowserTabActivity como elemento secundario del elemento <application>. Esta entrada permite a Microsoft Entra ID volver a llamar a la aplicación una vez completada la autenticación:

    <!--Intent filter to capture System Browser or Authenticator calling back to our app after sign-in-->
    <activity
        android:name="com.microsoft.identity.client.BrowserTabActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="msauth"
                android:host="Enter_the_Package_Name"
                android:path="/Enter_the_Signature_Hash" />
        </intent-filter>
    </activity>
    
    • Use el nombre del paquete para reemplazar el valor android:host=.. Debería ser parecido a este: com.azuresamples.msalandroidapp.
    • Use el hash de firma para reemplazar el valor android:path=. Asegúrate de que haya un / interlineado inicial al principio del Hash de firma. Debería ser parecido a este: /1wIqXSqBj7w+h11ZifsnqwgyKrY=.

    También puedes encontrar estos valores en la hoja de Autenticación del registro de la aplicación.

Agrega MSAL y bibliotecas relevantes al proyecto

  1. En la ventana del proyecto de Android Studio, ve a app>build.gradle y agrega las siguientes bibliotecas en la sección dependencias :

     implementation 'com.microsoft.identity.client:msal:5.0.0'
     implementation 'com.android.volley:volley:1.2.1'
    
  2. En la ventana del proyecto de Android Studio, abre settings.gradle y declara el siguiente repositorio maven en la sección de repositorios dependencyResolutionManagement>:

     maven {
          url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1'
     }
    
  3. Selecciona Sincronizar ahora en la barra de notificaciones.

Crear y actualizar el fragmento necesario

  1. En app>src>main>java>com.example(your app name). Crea los siguientes fragmentos de Android:

    • MSGraphRequestWrapper
    • OnFragmentInteractionListener
    • SingleAccountModeFragment
  2. Abre MSGraphRequestWrapper.java y reemplaza el código por el siguiente fragmento de código para llamar a Microsoft Graph API mediante el token proporcionado por MSAL:

     package com.azuresamples.msalandroidapp;
    
     import android.content.Context;
     import android.util.Log;
    
     import androidx.annotation.NonNull;
    
     import com.android.volley.DefaultRetryPolicy;
     import com.android.volley.Request;
     import com.android.volley.RequestQueue;
     import com.android.volley.Response;
     import com.android.volley.toolbox.JsonObjectRequest;
     import com.android.volley.toolbox.Volley;
    
     import org.json.JSONObject;
    
     import java.util.HashMap;
     import java.util.Map;
    
     public class MSGraphRequestWrapper {
         private static final String TAG = MSGraphRequestWrapper.class.getSimpleName();
    
         // See: https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints
         public static final String MS_GRAPH_ROOT_ENDPOINT = "https://graph.microsoft.com/";
    
         /**
          * Use Volley to make an HTTP request with
          * 1) a given MSGraph resource URL
          * 2) an access token
          * to obtain MSGraph data.
          **/
         public static void callGraphAPIUsingVolley(@NonNull final Context context,
                                                    @NonNull final String graphResourceUrl,
                                                    @NonNull final String accessToken,
                                                    @NonNull final Response.Listener<JSONObject> responseListener,
                                                    @NonNull final Response.ErrorListener errorListener) {
             Log.d(TAG, "Starting volley request to graph");
    
             /* Make sure we have a token to send to graph */
             if (accessToken == null || accessToken.length() == 0) {
                 return;
             }
    
             RequestQueue queue = Volley.newRequestQueue(context);
             JSONObject parameters = new JSONObject();
    
             try {
                 parameters.put("key", "value");
             } catch (Exception e) {
                 Log.d(TAG, "Failed to put parameters: " + e.toString());
             }
    
             JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, graphResourceUrl,
                     parameters, responseListener, errorListener) {
                 @Override
                 public Map<String, String> getHeaders() {
                     Map<String, String> headers = new HashMap<>();
                     headers.put("Authorization", "Bearer " + accessToken);
                     return headers;
                 }
             };
    
             Log.d(TAG, "Adding HTTP GET to Queue, Request: " + request.toString());
    
             request.setRetryPolicy(new DefaultRetryPolicy(
                     3000,
                     DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
                     DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
             queue.add(request);
         }
     }
    
  3. Abre OnFragmentInteractionListener.java y reemplaza el código por el siguiente fragmento de código para permitir la comunicación entre distintos fragmentos:

     package com.azuresamples.msalandroidapp;
    
     /**
      * This interface must be implemented by activities that contain this
      * fragment to allow an interaction in this fragment to be communicated
      * to the activity and potentially other fragments contained in that
      * activity.
      * <p>
      * See the Android Training lesson <a href=
      * "http://developer.android.com/training/basics/fragments/communicating.html"
      * >Communicating with Other Fragments</a> for more information.
      */
     public interface OnFragmentInteractionListener {
     }
    
  4. Abre SingleAccountModeFragment.java y reemplaza el código por el siguiente fragmento de código para inicializar una aplicación de una sola cuenta, carga una cuenta de usuario y obtiene un token para llamar al Graph API de Microsoft:

     package com.azuresamples.msalandroidapp;
    
     import android.os.Bundle;
    
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.fragment.app.Fragment;
    
     import android.util.Log;
     import android.view.LayoutInflater;
     import android.view.View;
     import android.view.ViewGroup;
     import android.widget.Button;
     import android.widget.TextView;
     import android.widget.Toast;
    
     import com.android.volley.Response;
     import com.android.volley.VolleyError;
     import com.microsoft.identity.client.AuthenticationCallback;
     import com.microsoft.identity.client.IAccount;
     import com.microsoft.identity.client.IAuthenticationResult;
     import com.microsoft.identity.client.IPublicClientApplication;
     import com.microsoft.identity.client.ISingleAccountPublicClientApplication;
     import com.microsoft.identity.client.PublicClientApplication;
     import com.microsoft.identity.client.SilentAuthenticationCallback;
     import com.microsoft.identity.client.exception.MsalClientException;
     import com.microsoft.identity.client.exception.MsalException;
     import com.microsoft.identity.client.exception.MsalServiceException;
     import com.microsoft.identity.client.exception.MsalUiRequiredException;
    
     import org.json.JSONObject;
    
     /**
      * Implementation sample for 'Single account' mode.
      * <p>
      * If your app only supports one account being signed-in at a time, this is for you.
      * This requires "account_mode" to be set as "SINGLE" in the configuration file.
      * (Please see res/raw/auth_config_single_account.json for more info).
      * <p>
      * Please note that switching mode (between 'single' and 'multiple' might cause a loss of data.
      */
     public class SingleAccountModeFragment extends Fragment {
         private static final String TAG = SingleAccountModeFragment.class.getSimpleName();
    
         /* UI & Debugging Variables */
         Button signInButton;
         Button signOutButton;
         Button callGraphApiInteractiveButton;
         Button callGraphApiSilentButton;
         TextView scopeTextView;
         TextView graphResourceTextView;
         TextView logTextView;
         TextView currentUserTextView;
         TextView deviceModeTextView;
    
         /* Azure AD Variables */
         private ISingleAccountPublicClientApplication mSingleAccountApp;
         private IAccount mAccount;
    
         @Override
         public View onCreateView(LayoutInflater inflater,
                                  ViewGroup container,
                                  Bundle savedInstanceState) {
             // Inflate the layout for this fragment
             final View view = inflater.inflate(R.layout.fragment_single_account_mode, container, false);
             initializeUI(view);
    
             // Creates a PublicClientApplication object with res/raw/auth_config_single_account.json
             PublicClientApplication.createSingleAccountPublicClientApplication(getContext(),
                     R.raw.auth_config_single_account,
                     new IPublicClientApplication.ISingleAccountApplicationCreatedListener() {
                         @Override
                         public void onCreated(ISingleAccountPublicClientApplication application) {
                             /**
                              * This test app assumes that the app is only going to support one account.
                              * This requires "account_mode" : "SINGLE" in the config json file.
                              **/
                             mSingleAccountApp = application;
                             loadAccount();
                         }
    
                         @Override
                         public void onError(MsalException exception) {
                             displayError(exception);
                         }
                     });
    
             return view;
         }
    
         /**
          * Initializes UI variables and callbacks.
          */
         private void initializeUI(@NonNull final View view) {
             signInButton = view.findViewById(R.id.btn_signIn);
             signOutButton = view.findViewById(R.id.btn_removeAccount);
             callGraphApiInteractiveButton = view.findViewById(R.id.btn_callGraphInteractively);
             callGraphApiSilentButton = view.findViewById(R.id.btn_callGraphSilently);
             scopeTextView = view.findViewById(R.id.scope);
             graphResourceTextView = view.findViewById(R.id.msgraph_url);
             logTextView = view.findViewById(R.id.txt_log);
             currentUserTextView = view.findViewById(R.id.current_user);
             deviceModeTextView = view.findViewById(R.id.device_mode);
    
             final String defaultGraphResourceUrl = MSGraphRequestWrapper.MS_GRAPH_ROOT_ENDPOINT + "v1.0/me";
             graphResourceTextView.setText(defaultGraphResourceUrl);
    
             signInButton.setOnClickListener(new View.OnClickListener() {
                 public void onClick(View v) {
                     if (mSingleAccountApp == null) {
                         return;
                     }
    
                     mSingleAccountApp.signIn(getActivity(), null, getScopes(), getAuthInteractiveCallback());
                 }
             });
    
             signOutButton.setOnClickListener(new View.OnClickListener() {
                 public void onClick(View v) {
                     if (mSingleAccountApp == null) {
                         return;
                     }
    
                     /**
                      * Removes the signed-in account and cached tokens from this app (or device, if the device is in shared mode).
                      */
                     mSingleAccountApp.signOut(new ISingleAccountPublicClientApplication.SignOutCallback() {
                         @Override
                         public void onSignOut() {
                             mAccount = null;
                             updateUI();
                             showToastOnSignOut();
                         }
    
                         @Override
                         public void onError(@NonNull MsalException exception) {
                             displayError(exception);
                         }
                     });
                 }
             });
    
             callGraphApiInteractiveButton.setOnClickListener(new View.OnClickListener() {
                 public void onClick(View v) {
                     if (mSingleAccountApp == null) {
                         return;
                     }
    
                     /**
                      * If acquireTokenSilent() returns an error that requires an interaction (MsalUiRequiredException),
                      * invoke acquireToken() to have the user resolve the interrupt interactively.
                      *
                      * Some example scenarios are
                      *  - password change
                      *  - the resource you're acquiring a token for has a stricter set of requirement than your Single Sign-On refresh token.
                      *  - you're introducing a new scope which the user has never consented for.
                      */
                     mSingleAccountApp.acquireToken(getActivity(), getScopes(), getAuthInteractiveCallback());
                 }
             });
    
             callGraphApiSilentButton.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     if (mSingleAccountApp == null) {
                         return;
                     }
    
                     /**
                      * Once you've signed the user in,
                      * you can perform acquireTokenSilent to obtain resources without interrupting the user.
                      */
                     mSingleAccountApp.acquireTokenSilentAsync(getScopes(), mAccount.getAuthority(), getAuthSilentCallback());
                 }
             });
    
         }
    
         @Override
         public void onResume() {
             super.onResume();
    
             /**
              * The account may have been removed from the device (if broker is in use).
              *
              * In shared device mode, the account might be signed in/out by other apps while this app is not in focus.
              * Therefore, we want to update the account state by invoking loadAccount() here.
              */
             loadAccount();
         }
    
         /**
          * Extracts a scope array from a text field,
          * i.e. from "User.Read User.ReadWrite" to ["user.read", "user.readwrite"]
          */
         private String[] getScopes() {
             return scopeTextView.getText().toString().toLowerCase().split(" ");
         }
    
         /**
          * Load the currently signed-in account, if there's any.
          */
         private void loadAccount() {
             if (mSingleAccountApp == null) {
                 return;
             }
    
             mSingleAccountApp.getCurrentAccountAsync(new ISingleAccountPublicClientApplication.CurrentAccountCallback() {
                 @Override
                 public void onAccountLoaded(@Nullable IAccount activeAccount) {
                     // You can use the account data to update your UI or your app database.
                     mAccount = activeAccount;
                     updateUI();
                 }
    
                 @Override
                 public void onAccountChanged(@Nullable IAccount priorAccount, @Nullable IAccount currentAccount) {
                     if (currentAccount == null) {
                         // Perform a cleanup task as the signed-in account changed.
                         showToastOnSignOut();
                     }
                 }
    
                 @Override
                 public void onError(@NonNull MsalException exception) {
                     displayError(exception);
                 }
             });
         }
    
         /**
          * Callback used in for silent acquireToken calls.
          */
         private SilentAuthenticationCallback getAuthSilentCallback() {
             return new SilentAuthenticationCallback() {
    
                 @Override
                 public void onSuccess(IAuthenticationResult authenticationResult) {
                     Log.d(TAG, "Successfully authenticated");
    
                     /* Successfully got a token, use it to call a protected resource - MSGraph */
                     callGraphAPI(authenticationResult);
                 }
    
                 @Override
                 public void onError(MsalException exception) {
                     /* Failed to acquireToken */
                     Log.d(TAG, "Authentication failed: " + exception.toString());
                     displayError(exception);
    
                     if (exception instanceof MsalClientException) {
                         /* Exception inside MSAL, more info inside MsalError.java */
                     } else if (exception instanceof MsalServiceException) {
                         /* Exception when communicating with the STS, likely config issue */
                     } else if (exception instanceof MsalUiRequiredException) {
                         /* Tokens expired or no session, retry with interactive */
                     }
                 }
             };
         }
    
         /**
          * Callback used for interactive request.
          * If succeeds we use the access token to call the Microsoft Graph.
          * Does not check cache.
          */
         private AuthenticationCallback getAuthInteractiveCallback() {
             return new AuthenticationCallback() {
    
                 @Override
                 public void onSuccess(IAuthenticationResult authenticationResult) {
                     /* Successfully got a token, use it to call a protected resource - MSGraph */
                     Log.d(TAG, "Successfully authenticated");
                     Log.d(TAG, "ID Token: " + authenticationResult.getAccount().getClaims().get("id_token"));
    
                     /* Update account */
                     mAccount = authenticationResult.getAccount();
                     updateUI();
    
                     /* call graph */
                     callGraphAPI(authenticationResult);
                 }
    
                 @Override
                 public void onError(MsalException exception) {
                     /* Failed to acquireToken */
                     Log.d(TAG, "Authentication failed: " + exception.toString());
                     displayError(exception);
    
                     if (exception instanceof MsalClientException) {
                         /* Exception inside MSAL, more info inside MsalError.java */
                     } else if (exception instanceof MsalServiceException) {
                         /* Exception when communicating with the STS, likely config issue */
                     }
                 }
    
                 @Override
                 public void onCancel() {
                     /* User canceled the authentication */
                     Log.d(TAG, "User cancelled login.");
                 }
             };
         }
    
         /**
          * Make an HTTP request to obtain MSGraph data
          */
         private void callGraphAPI(final IAuthenticationResult authenticationResult) {
             MSGraphRequestWrapper.callGraphAPIUsingVolley(
                     getContext(),
                     graphResourceTextView.getText().toString(),
                     authenticationResult.getAccessToken(),
                     new Response.Listener<JSONObject>() {
                         @Override
                         public void onResponse(JSONObject response) {
                             /* Successfully called graph, process data and send to UI */
                             Log.d(TAG, "Response: " + response.toString());
                             displayGraphResult(response);
                         }
                     },
                     new Response.ErrorListener() {
                         @Override
                         public void onErrorResponse(VolleyError error) {
                             Log.d(TAG, "Error: " + error.toString());
                             displayError(error);
                         }
                     });
         }
    
         //
         // Helper methods manage UI updates
         // ================================
         // displayGraphResult() - Display the graph response
         // displayError() - Display the graph response
         // updateSignedInUI() - Updates UI when the user is signed in
         // updateSignedOutUI() - Updates UI when app sign out succeeds
         //
    
         /**
          * Display the graph response
          */
         private void displayGraphResult(@NonNull final JSONObject graphResponse) {
             logTextView.setText(graphResponse.toString());
         }
    
         /**
          * Display the error message
          */
         private void displayError(@NonNull final Exception exception) {
             logTextView.setText(exception.toString());
         }
    
         /**
          * Updates UI based on the current account.
          */
         private void updateUI() {
             if (mAccount != null) {
                 signInButton.setEnabled(false);
                 signOutButton.setEnabled(true);
                 callGraphApiInteractiveButton.setEnabled(true);
                 callGraphApiSilentButton.setEnabled(true);
                 currentUserTextView.setText(mAccount.getUsername());
             } else {
                 signInButton.setEnabled(true);
                 signOutButton.setEnabled(false);
                 callGraphApiInteractiveButton.setEnabled(false);
                 callGraphApiSilentButton.setEnabled(false);
                 currentUserTextView.setText("None");
             }
    
             deviceModeTextView.setText(mSingleAccountApp.isSharedDevice() ? "Shared" : "Non-shared");
         }
    
         /**
          * Updates UI when app sign out succeeds
          */
         private void showToastOnSignOut() {
             final String signOutText = "Signed Out.";
             currentUserTextView.setText("");
             Toast.makeText(getContext(), signOutText, Toast.LENGTH_SHORT)
                     .show();
         }
     }
    
  5. Abre MainActivity.java y reemplaza el código por el siguiente fragmento de código para administrar la interfaz de usuario.

     package com.azuresamples.msalandroidapp;
    
     import android.os.Bundle;
    
     import androidx.annotation.NonNull;
     import androidx.appcompat.app.ActionBarDrawerToggle;
     import androidx.appcompat.app.AppCompatActivity;
     import androidx.appcompat.widget.Toolbar;
     import androidx.constraintlayout.widget.ConstraintLayout;
     import androidx.core.view.GravityCompat;
    
     import android.view.MenuItem;
     import android.view.View;
    
     import androidx.drawerlayout.widget.DrawerLayout;
     import androidx.fragment.app.Fragment;
     import androidx.fragment.app.FragmentTransaction;
    
    
     import com.google.android.material.navigation.NavigationView;
    
     public class MainActivity extends AppCompatActivity
             implements NavigationView.OnNavigationItemSelectedListener,
             OnFragmentInteractionListener{
    
         enum AppFragment {
             SingleAccount
         }
    
         private AppFragment mCurrentFragment;
    
         private ConstraintLayout mContentMain;
    
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
    
             mContentMain = findViewById(R.id.content_main);
    
             Toolbar toolbar = findViewById(R.id.toolbar);
             setSupportActionBar(toolbar);
             DrawerLayout drawer = findViewById(R.id.drawer_layout);
             NavigationView navigationView = findViewById(R.id.nav_view);
             ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                     this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
             drawer.addDrawerListener(toggle);
             toggle.syncState();
             navigationView.setNavigationItemSelectedListener(this);
    
             //Set default fragment
             navigationView.setCheckedItem(R.id.nav_single_account);
             setCurrentFragment(AppFragment.SingleAccount);
         }
    
         @Override
         public boolean onNavigationItemSelected(final MenuItem item) {
             final DrawerLayout drawer = findViewById(R.id.drawer_layout);
             drawer.addDrawerListener(new DrawerLayout.DrawerListener() {
                 @Override
                 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { }
    
                 @Override
                 public void onDrawerOpened(@NonNull View drawerView) { }
    
                 @Override
                 public void onDrawerClosed(@NonNull View drawerView) {
                     // Handle navigation view item clicks here.
                     int id = item.getItemId();
    
                     if (id == R.id.nav_single_account) {
                         setCurrentFragment(AppFragment.SingleAccount);
                     }
    
    
                     drawer.removeDrawerListener(this);
                 }
    
                 @Override
                 public void onDrawerStateChanged(int newState) { }
             });
    
             drawer.closeDrawer(GravityCompat.START);
             return true;
         }
    
         private void setCurrentFragment(final AppFragment newFragment){
             if (newFragment == mCurrentFragment) {
                 return;
             }
    
             mCurrentFragment = newFragment;
             setHeaderString(mCurrentFragment);
             displayFragment(mCurrentFragment);
         }
    
         private void setHeaderString(final AppFragment fragment){
             switch (fragment) {
                 case SingleAccount:
                     getSupportActionBar().setTitle("Single Account Mode");
                     return;
    
             }
         }
    
         private void displayFragment(final AppFragment fragment){
             switch (fragment) {
                 case SingleAccount:
                     attachFragment(new com.azuresamples.msalandroidapp.SingleAccountModeFragment());
                     return;
    
             }
         }
    
         private void attachFragment(final Fragment fragment) {
             getSupportFragmentManager()
                     .beginTransaction()
                     .setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
                     .replace(mContentMain.getId(),fragment)
                     .commit();
         }
     }
    

Nota

Asegúrate de actualizar el nombre del paquete para que coincida con el nombre del paquete del proyecto de Android.

Layout

Un diseño es un archivo que define la estructura visual y la apariencia de una interfaz de usuario, especificando la disposición de los componentes de la interfaz de usuario. Está escrito en XML. Si quiere modelar la interfaz de usuario de este tutorial, se proporcionan los siguientes ejemplos XML:

  1. En app>src>main>res>layout>activity_main.xml. Reemplaza el contenido de activity_main.xml por el siguiente fragmento de código para mostrar botones y cuadros de texto:

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         xmlns:tools="http://schemas.android.com/tools"
         android:id="@+id/drawer_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:fitsSystemWindows="true"
         tools:openDrawer="start">
    
         <include
             layout="@layout/app_bar_main"
             android:layout_width="match_parent"
             android:layout_height="match_parent" />
    
         <com.google.android.material.navigation.NavigationView
             android:id="@+id/nav_view"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
             android:layout_gravity="start"
             android:fitsSystemWindows="true"
             app:headerLayout="@layout/nav_header_main"
             app:menu="@menu/activity_main_drawer" />
    
     </androidx.drawerlayout.widget.DrawerLayout>
    
  2. En app>src>main>res>layout>app_bar_main.xml. Si no tiene app_bar_main.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">
    
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay" />
    
        </com.google.android.material.appbar.AppBarLayout>
    
        <include layout="@layout/content_main" />
    
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
  3. En app>src>main>res>layout>content_main.xml. Si no tiene content_main.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content_main"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".MainActivity"
        tools:showIn="@layout/app_bar_main">
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  4. En app>src>main>res>layout>fragment_m_s_graph_request_wrapper.xml. Si no tiene fragment_m_s_graph_request_wrapper.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MSGraphRequestWrapper">
    
        <!-- TODO: Update blank fragment layout -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/hello_blank_fragment" />
    
    </FrameLayout>
    
  5. En app>src>main>res>layout>fragment_on_interaction_listener.xml. Si no tiene fragment_on_interaction_listener.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".OnFragmentInteractionListener">
    
        <!-- TODO: Update blank fragment layout -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/hello_blank_fragment" />
    
    </FrameLayout>
    
  6. En app>src>main>res>layout>fragment_single_account_mode.xml. Si no tiene fragment_single_account_mode.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".SingleAccountModeFragment">
    
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:context=".SingleAccountModeFragment">
    
            <LinearLayout
                android:id="@+id/activity_main"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingBottom="@dimen/activity_vertical_margin">
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:paddingTop="5dp"
                    android:paddingBottom="5dp"
                    android:weightSum="10">
    
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="3"
                        android:layout_gravity="center_vertical"
                        android:textStyle="bold"
                        android:text="Scope" />
    
                    <LinearLayout
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:orientation="vertical"
                        android:layout_weight="7">
    
                        <EditText
                            android:id="@+id/scope"
                            android:layout_height="wrap_content"
                            android:layout_width="match_parent"
                            android:text="user.read"
                            android:textSize="12sp" />
    
                        <TextView
                            android:layout_height="wrap_content"
                            android:layout_width="match_parent"
                            android:paddingLeft="5dp"
                            android:text="Type in scopes delimited by space"
                            android:textSize="10sp"  />
    
                    </LinearLayout>
                </LinearLayout>
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:paddingTop="5dp"
                    android:paddingBottom="5dp"
                    android:weightSum="10">
    
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="3"
                        android:layout_gravity="center_vertical"
                        android:textStyle="bold"
                        android:text="MSGraph Resource URL" />
    
                    <LinearLayout
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:orientation="vertical"
                        android:layout_weight="7">
    
                        <EditText
                            android:id="@+id/msgraph_url"
                            android:layout_height="wrap_content"
                            android:layout_width="match_parent"
                            android:textSize="12sp" />
                    </LinearLayout>
                </LinearLayout>
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:paddingTop="5dp"
                    android:paddingBottom="5dp"
                    android:weightSum="10">
    
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="3"
                        android:textStyle="bold"
                        android:text="Signed-in user" />
    
                    <TextView
                        android:id="@+id/current_user"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:paddingLeft="5dp"
                        android:layout_weight="7"
                        android:text="None" />
                </LinearLayout>
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:paddingTop="5dp"
                    android:paddingBottom="5dp"
                    android:weightSum="10">
    
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="3"
                        android:textStyle="bold"
                        android:text="Device mode" />
    
                    <TextView
                        android:id="@+id/device_mode"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:paddingLeft="5dp"
                        android:layout_weight="7"
                        android:text="None" />
                </LinearLayout>
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:paddingTop="5dp"
                    android:paddingBottom="5dp"
                    android:weightSum="10">
    
                    <Button
                        android:id="@+id/btn_signIn"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="5"
                        android:gravity="center"
                        android:text="Sign In"/>
    
                    <Button
                        android:id="@+id/btn_removeAccount"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="5"
                        android:gravity="center"
                        android:text="Sign Out"
                        android:enabled="false"/>
                </LinearLayout>
    
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:orientation="horizontal">
    
                    <Button
                        android:id="@+id/btn_callGraphInteractively"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="5"
                        android:text="Get Graph Data Interactively"
                        android:enabled="false"/>
    
                    <Button
                        android:id="@+id/btn_callGraphSilently"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="5"
                        android:text="Get Graph Data Silently"
                        android:enabled="false"/>
                </LinearLayout>
    
    
                <TextView
                    android:id="@+id/txt_log"
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_marginTop="20dp"
                    android:layout_weight="0.8"
                    android:text="Output goes here..." />
    
            </LinearLayout>
        </LinearLayout>
    
    </FrameLayout>
    
  7. En app>src>main>res>layout>nav_header_main.xml. Si no tiene nav_header_main.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="@dimen/nav_header_height"
        android:background="@drawable/side_nav_bar"
        android:gravity="bottom"
        android:orientation="vertical"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:theme="@style/ThemeOverlay.AppCompat.Dark">
    
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="66dp"
            android:layout_height="72dp"
            android:contentDescription="@string/nav_header_desc"
            android:paddingTop="@dimen/nav_header_vertical_spacing"
            app:srcCompat="@drawable/microsoft_logo" />
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="@dimen/nav_header_vertical_spacing"
            android:text="Azure Samples"
            android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="MSAL Android" />
    
    </LinearLayout>
    
    
  8. En el menú>res>principal>src>de la aplicación>activity_main_drawer.xml. Si no tiene activity_main_drawer.xml en su carpeta, cree y agregue el siguiente fragmento de código:

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        tools:showIn="navigation_view">
        <group android:checkableBehavior="single">
            <item
                android:id="@+id/nav_single_account"
                android:icon="@drawable/ic_single_account_24dp"
                android:title="Single Account Mode" />
    
        </group>
    </menu>
    
  9. En app>src>main>res>values>dimens.xml. Reemplace el contenido de dimens.xml con el fragmento de código siguiente:

    <resources>
        <dimen name="fab_margin">16dp</dimen>
        <dimen name="activity_horizontal_margin">16dp</dimen>
        <dimen name="activity_vertical_margin">16dp</dimen>
        <dimen name="nav_header_height">176dp</dimen>
        <dimen name="nav_header_vertical_spacing">8dp</dimen>
    </resources>
    
  10. En app>src>main>res>values>colors.xml. Reemplace el contenido de colors.xml con el fragmento de código siguiente:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <color name="purple_200">#FFBB86FC</color>
        <color name="purple_500">#FF6200EE</color>
        <color name="purple_700">#FF3700B3</color>
        <color name="teal_200">#FF03DAC5</color>
        <color name="teal_700">#FF018786</color>
        <color name="black">#FF000000</color>
        <color name="white">#FFFFFFFF</color>
        <color name="colorPrimary">#008577</color>
        <color name="colorPrimaryDark">#00574B</color>
        <color name="colorAccent">#D81B60</color>
    </resources>
    
  11. En losvalores>res>principales>src>de la aplicación>strings.xml. Reemplace el contenido de strings.xml con el fragmento de código siguiente:

    <resources>
        <string name="app_name">MSALAndroidapp</string>
        <string name="action_settings">Settings</string>
        <!-- Strings used for fragments for navigation -->
        <string name="first_fragment_label">First Fragment</string>
        <string name="second_fragment_label">Second Fragment</string>
        <string name="nav_header_desc">Navigation header</string>
        <string name="navigation_drawer_open">Open navigation drawer</string>
        <string name="navigation_drawer_close">Close navigation drawer</string>
        <string name="next">Next</string>
        <string name="previous">Previous</string>
    
        <string name="hello_first_fragment">Hello first fragment</string>
        <string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
        <!-- TODO: Remove or change this placeholder text -->
        <string name="hello_blank_fragment">Hello blank fragment</string>
    </resources>
    
  12. En app>src>main>res>values>styles.xml. Si no tiene styles.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <resources>
    
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    
    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
    
    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
    
    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
    
    </resources>
    
  13. En app>src>main>res>values>themes.xml. Reemplace el contenido de themes.xml con el fragmento de código siguiente:

    <resources xmlns:tools="http://schemas.android.com/tools">
        <!-- Base application theme. -->
        <style name="Theme.MSALAndroidapp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
            <!-- Primary brand color. -->
            <item name="colorPrimary">@color/purple_500</item>
            <item name="colorPrimaryVariant">@color/purple_700</item>
            <item name="colorOnPrimary">@color/white</item>
            <!-- Secondary brand color. -->
            <item name="colorSecondary">@color/teal_200</item>
            <item name="colorSecondaryVariant">@color/teal_700</item>
            <item name="colorOnSecondary">@color/black</item>
            <!-- Status bar color. -->
            <item name="android:statusBarColor" tools:targetApi="21">?attr/colorPrimaryVariant</item>
            <!-- Customize your theme here. -->
        </style>
    
        <style name="Theme.MSALAndroidapp.NoActionBar">
            <item name="windowActionBar">false</item>
            <item name="windowNoTitle">true</item>
        </style>
    
        <style name="Theme.MSALAndroidapp.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
    
        <style name="Theme.MSALAndroidapp.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
    </resources>
    
  14. En app>src>main>res>drawable>ic_single_account_24dp.xml. Si no tiene ic_single_account_24dp.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
    </vector>
    
  15. En app>src>main>res>drawable>side_nav_bar.xml. Si no tiene side_nav_bar.xml en la carpeta, cree y agregue el siguiente fragmento de código:

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="135"
        android:centerColor="#009688"
        android:endColor="#00695C"
        android:startColor="#4DB6AC"
        android:type="linear" />
    </shape>
    
  16. En app>src>main>res>drawable. En la carpeta, agregue un logotipo png de Microsoft denominado microsoft_logo.png.

Declarar la interfaz de usuario en XML permite separar la presentación de la aplicación del código que controla su comportamiento. Para más información sobre el diseño de Android, consulte Diseños.

Prueba de la aplicación

Ejecución en modo local

Cree e implemente la aplicación en un emulador o dispositivo de prueba. Debe ser capaz de iniciar sesión y obtener tokens para Microsoft Entra ID o cuentas personales de Microsoft.

Después de iniciar sesión, esta aplicación mostrará los datos devueltos desde el punto de conexión /me de Microsoft Graph.

La primera vez que un usuario inicie sesión en tu aplicación, la identidad de Microsoft le pedirá que acepte los permisos solicitados. Algunos inquilinos de Microsoft Entra tienen el consentimiento del usuario deshabilitado, lo que requiere que los administradores den su consentimiento en nombre de todos los usuarios. Para admitir este escenario, tendrá que crear su propio inquilino o recibir el consentimiento del administrador.

Limpieza de recursos

Cuando ya no lo necesite, elimine el objeto de aplicación que creó en el paso Registrar su aplicación.

Ayuda y soporte técnico

Si necesita ayuda, desea informar de un problema o desea obtener información sobre las opciones de soporte técnico, consulte Opciones de ayuda y soporte técnico para desarrolladores.

Pasos siguientes

Para explorar escenarios más complejos, consulte un ejemplo de código de trabajo completado en GitHub.

Para obtener más información sobre la creación de aplicaciones móviles que llamen a las API web protegidas en nuestra serie de escenarios de varias partes, consulta: