title | description | author | manager | ms.author | ms.custom | ms.date | ms.reviewer | ms.service | ms.topic |
---|---|---|---|---|---|---|---|---|---|
Tutorial: Create an Android app that uses the Microsoft identity platform for authentication | In this tutorial, you build an Android app that uses the Microsoft identity platform to sign in users and get an access token to call the Microsoft Graph API on their behalf. | henrymbuguakiarie | CelesteDG | henrymbugua | 02/24/2024 | negoe | identity-platform | tutorial |
In this tutorial, you build an Android app that integrates with the Microsoft Entra ID to sign in users and get an access token to call the Microsoft Graph API.
When you've completed this tutorial, your application accepts sign-ins of personal Microsoft accounts (including outlook.com, live.com, and others) and work or school accounts from any company or organization that uses Microsoft Entra ID.
In this tutorial, you:
[!div class="checklist"]
- Create an Android app project in Android Studio
- Register the app in the Microsoft Entra admin center
- Add code to support user sign-in and sign-out
- Add code to call the Microsoft Graph API
- Test the app
The app in this tutorial signs in users and get data on their behalf. This data is accessed through a protected API (Microsoft Graph API) that requires authorization and is protected by the Microsoft identity platform.
This sample uses the Microsoft Authentication Library (MSAL) for Android to implement Authentication: com.microsoft.identity.client.
Follow these steps to create a new project if you don't already have an Android application.
- Open Android Studio, and select Start a new Android Studio project.
- Select Basic Activity and select Next.
- Enter a name for the application, such as MSALAndroidapp.
- Record the package name to be used in later steps.
- Change the language from Kotlin to Java.
- Set the Minimum SDK API level to API 16 or higher, and select Finish.
Sign in to the Microsoft Entra admin center as at least an Application Developer.
If you have access to multiple tenants, use the Settings icon :::image type="icon" source="media/common/admin-center-settings-icon.png" border="false"::: in the top menu to switch to the tenant in which you want to register the application from the Directories + subscriptions menu.
Browse to Entra ID > App registrations.
Select New registration.
Enter a Name for your application. Users of your app might see this name, and you can change it later.
For Supported account types, select Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox). For information on different account types, select the Help me choose option.
Select Register.
Under Manage, select Authentication > Add a platform > Android.
Enter your project's Package Name. If you downloaded the sample code, this value is
com.azuresamples.msalandroidapp
.In the Signature hash section of the Configure your Android app pane, select Generating a development Signature Hash. and copy the KeyTool command to your command line.
- KeyTool.exe is installed as part of the Java Development Kit (JDK). You must also install the OpenSSL tool to execute the KeyTool command. For more information, see Android documentation on generating a key for more information.
Enter the Signature hash generated by KeyTool.
Select Configure and save the MSAL Configuration that appears in the Android configuration pane so you can enter it when you configure your app later.
Select Done.
In Android Studio's project pane, navigate to app\src\main\res.
Right-click res and choose New > Directory. Enter
raw
as the new directory name and select OK.In app > src > main > res > raw, create a new JSON file called
auth_config_single_account.json
and paste the MSAL Configuration that you saved earlier.Below the redirect URI, paste:
"account_mode" : "SINGLE",
Your config file should resemble this example:
{ "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" } } ] }
As this tutorial only demonstrates how to configure an app in Single Account mode, see single vs. multiple account mode and configuring your app for more information
We recommend using 'WEBVIEW'. In case you want to configure "authorization_user_agent" as 'BROWSER' in your app, you need make the following updates. a) Update auth_config_single_account.json with "authorization_user_agent": "Browser". b) Update AndroidManifest.xml. In the app go to app > src > main > AndroidManifest.xml, add the
BrowserTabActivity
activity as a child of the<application>
element. This entry allows Microsoft Entra ID to call back to your application after it completes the authentication:<!--Intent filter to capture System Browser or Authenticator calling back to our app after sign-in--> <activityandroid:name="com.microsoft.identity.client.BrowserTabActivity"android:exported="true"> <intent-filter> <actionandroid:name="android.intent.action.VIEW" /> <categoryandroid:name="android.intent.category.DEFAULT" /> <categoryandroid:name="android.intent.category.BROWSABLE" /> <dataandroid:scheme="msauth"android:host="Enter_the_Package_Name"android:path="/Enter_the_Signature_Hash" /> </intent-filter> </activity>
- Use the Package name to replace
android:host=.
value. It should look likecom.azuresamples.msalandroidapp
. - Use the Signature Hash to replace
android:path=
value. Ensure that there's a leading/
at the beginning of your Signature Hash. It should look like/aB1cD2eF3gH4+iJ5kL6-mN7oP8q=
.
You can find these values in the Authentication blade of your app registration as well.
- Use the Package name to replace
In the Android Studio project window, navigate to app > build.gradle and add the following libraries in the dependencies section:
implementation 'com.microsoft.identity.client:msal:5.0.0' implementation 'com.android.volley:volley:1.2.1'
In the Android Studio project window, open settings.gradle and declare the following maven repository in dependencyResolutionManagement > repositories section:
maven { url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' }
Select Sync Now in the notification bar.
In app > src > main> java > com.example(your app name). Create the following Android fragments:
- MSGraphRequestWrapper
- OnFragmentInteractionListener
- SingleAccountModeFragment
Open MSGraphRequestWrapper.java and replace the code with following code snippet to call the Microsoft Graph API using the token provided by MSAL:
packagecom.azuresamples.msalandroidapp; importandroid.content.Context; importandroid.util.Log; importandroidx.annotation.NonNull; importcom.android.volley.DefaultRetryPolicy; importcom.android.volley.Request; importcom.android.volley.RequestQueue; importcom.android.volley.Response; importcom.android.volley.toolbox.JsonObjectRequest; importcom.android.volley.toolbox.Volley; importorg.json.JSONObject; importjava.util.HashMap; importjava.util.Map; publicclassMSGraphRequestWrapper { privatestaticfinalStringTAG = MSGraphRequestWrapper.class.getSimpleName(); // See: https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpointspublicstaticfinalStringMS_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. **/publicstaticvoidcallGraphAPIUsingVolley(@NonNullfinalContextcontext, @NonNullfinalStringgraphResourceUrl, @NonNullfinalStringaccessToken, @NonNullfinalResponse.Listener<JSONObject> responseListener, @NonNullfinalResponse.ErrorListenererrorListener) { 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; } RequestQueuequeue = Volley.newRequestQueue(context); JSONObjectparameters = newJSONObject(); try { parameters.put("key", "value"); } catch (Exceptione) { Log.d(TAG, "Failed to put parameters: " + e.toString()); } JsonObjectRequestrequest = newJsonObjectRequest(Request.Method.GET, graphResourceUrl, parameters, responseListener, errorListener) { @OverridepublicMap<String, String> getHeaders() { Map<String, String> headers = newHashMap<>(); headers.put("Authorization", "Bearer " + accessToken); returnheaders; } }; Log.d(TAG, "Adding HTTP GET to Queue, Request: " + request.toString()); request.setRetryPolicy(newDefaultRetryPolicy( 3000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); queue.add(request); } }
Open OnFragmentInteractionListener.java and replace the code with following code snippet to allow communication between different fragments:
packagecom.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. */publicinterfaceOnFragmentInteractionListener { }
Open SingleAccountModeFragment.java and replace the code with following code snippet to initialize a single-account application, loads a user account, and gets a token to call the Microsoft Graph API:
packagecom.azuresamples.msalandroidapp; importandroid.os.Bundle; importandroidx.annotation.NonNull; importandroidx.annotation.Nullable; importandroidx.fragment.app.Fragment; importandroid.util.Log; importandroid.view.LayoutInflater; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.Button; importandroid.widget.TextView; importandroid.widget.Toast; importcom.android.volley.Response; importcom.android.volley.VolleyError; importcom.microsoft.identity.client.AuthenticationCallback; importcom.microsoft.identity.client.IAccount; importcom.microsoft.identity.client.IAuthenticationResult; importcom.microsoft.identity.client.IPublicClientApplication; importcom.microsoft.identity.client.ISingleAccountPublicClientApplication; importcom.microsoft.identity.client.PublicClientApplication; importcom.microsoft.identity.client.SilentAuthenticationCallback; importcom.microsoft.identity.client.exception.MsalClientException; importcom.microsoft.identity.client.exception.MsalException; importcom.microsoft.identity.client.exception.MsalServiceException; importcom.microsoft.identity.client.exception.MsalUiRequiredException; importorg.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. */publicclassSingleAccountModeFragmentextendsFragment { privatestaticfinalStringTAG = SingleAccountModeFragment.class.getSimpleName(); /* UI & Debugging Variables */ButtonsignInButton; ButtonsignOutButton; ButtoncallGraphApiInteractiveButton; ButtoncallGraphApiSilentButton; TextViewscopeTextView; TextViewgraphResourceTextView; TextViewlogTextView; TextViewcurrentUserTextView; TextViewdeviceModeTextView; /* Azure AD Variables */privateISingleAccountPublicClientApplicationmSingleAccountApp; privateIAccountmAccount; @OverridepublicViewonCreateView(LayoutInflaterinflater, ViewGroupcontainer, BundlesavedInstanceState) { // Inflate the layout for this fragmentfinalViewview = inflater.inflate(R.layout.fragment_single_account_mode, container, false); initializeUI(view); // Creates a PublicClientApplication object with res/raw/auth_config_single_account.jsonPublicClientApplication.createSingleAccountPublicClientApplication(getContext(), R.raw.auth_config_single_account, newIPublicClientApplication.ISingleAccountApplicationCreatedListener() { @OverridepublicvoidonCreated(ISingleAccountPublicClientApplicationapplication) { /** * 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(); } @OverridepublicvoidonError(MsalExceptionexception) { displayError(exception); } }); returnview; } /** * Initializes UI variables and callbacks. */privatevoidinitializeUI(@NonNullfinalViewview) { 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); finalStringdefaultGraphResourceUrl = MSGraphRequestWrapper.MS_GRAPH_ROOT_ENDPOINT + "v1.0/me"; graphResourceTextView.setText(defaultGraphResourceUrl); signInButton.setOnClickListener(newView.OnClickListener() { publicvoidonClick(Viewv) { if (mSingleAccountApp == null) { return; } mSingleAccountApp.signIn(getActivity(), null, getScopes(), getAuthInteractiveCallback()); } }); signOutButton.setOnClickListener(newView.OnClickListener() { publicvoidonClick(Viewv) { 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(newISingleAccountPublicClientApplication.SignOutCallback() { @OverridepublicvoidonSignOut() { mAccount = null; updateUI(); showToastOnSignOut(); } @OverridepublicvoidonError(@NonNullMsalExceptionexception) { displayError(exception); } }); } }); callGraphApiInteractiveButton.setOnClickListener(newView.OnClickListener() { publicvoidonClick(Viewv) { 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(newView.OnClickListener() { @OverridepublicvoidonClick(Viewv) { 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()); } }); } @OverridepublicvoidonResume() { 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"] */privateString[] getScopes() { returnscopeTextView.getText().toString().toLowerCase().split(" "); } /** * Load the currently signed-in account, if there's any. */privatevoidloadAccount() { if (mSingleAccountApp == null) { return; } mSingleAccountApp.getCurrentAccountAsync(newISingleAccountPublicClientApplication.CurrentAccountCallback() { @OverridepublicvoidonAccountLoaded(@NullableIAccountactiveAccount) { // You can use the account data to update your UI or your app database.mAccount = activeAccount; updateUI(); } @OverridepublicvoidonAccountChanged(@NullableIAccountpriorAccount, @NullableIAccountcurrentAccount) { if (currentAccount == null) { // Perform a cleanup task as the signed-in account changed.showToastOnSignOut(); } } @OverridepublicvoidonError(@NonNullMsalExceptionexception) { displayError(exception); } }); } /** * Callback used in for silent acquireToken calls. */privateSilentAuthenticationCallbackgetAuthSilentCallback() { returnnewSilentAuthenticationCallback() { @OverridepublicvoidonSuccess(IAuthenticationResultauthenticationResult) { Log.d(TAG, "Successfully authenticated"); /* Successfully got a token, use it to call a protected resource - MSGraph */callGraphAPI(authenticationResult); } @OverridepublicvoidonError(MsalExceptionexception) { /* Failed to acquireToken */Log.d(TAG, "Authentication failed: " + exception.toString()); displayError(exception); if (exceptioninstanceofMsalClientException) { /* Exception inside MSAL, more info inside MsalError.java */ } elseif (exceptioninstanceofMsalServiceException) { /* Exception when communicating with the STS, likely config issue */ } elseif (exceptioninstanceofMsalUiRequiredException) { /* 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. */privateAuthenticationCallbackgetAuthInteractiveCallback() { returnnewAuthenticationCallback() { @OverridepublicvoidonSuccess(IAuthenticationResultauthenticationResult) { /* 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); } @OverridepublicvoidonError(MsalExceptionexception) { /* Failed to acquireToken */Log.d(TAG, "Authentication failed: " + exception.toString()); displayError(exception); if (exceptioninstanceofMsalClientException) { /* Exception inside MSAL, more info inside MsalError.java */ } elseif (exceptioninstanceofMsalServiceException) { /* Exception when communicating with the STS, likely config issue */ } } @OverridepublicvoidonCancel() { /* User canceled the authentication */Log.d(TAG, "User cancelled login."); } }; } /** * Make an HTTP request to obtain MSGraph data */privatevoidcallGraphAPI(finalIAuthenticationResultauthenticationResult) { MSGraphRequestWrapper.callGraphAPIUsingVolley( getContext(), graphResourceTextView.getText().toString(), authenticationResult.getAccessToken(), newResponse.Listener<JSONObject>() { @OverridepublicvoidonResponse(JSONObjectresponse) { /* Successfully called graph, process data and send to UI */Log.d(TAG, "Response: " + response.toString()); displayGraphResult(response); } }, newResponse.ErrorListener() { @OverridepublicvoidonErrorResponse(VolleyErrorerror) { 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 */privatevoiddisplayGraphResult(@NonNullfinalJSONObjectgraphResponse) { logTextView.setText(graphResponse.toString()); } /** * Display the error message */privatevoiddisplayError(@NonNullfinalExceptionexception) { logTextView.setText(exception.toString()); } /** * Updates UI based on the current account. */privatevoidupdateUI() { 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 */privatevoidshowToastOnSignOut() { finalStringsignOutText = "Signed Out."; currentUserTextView.setText(""); Toast.makeText(getContext(), signOutText, Toast.LENGTH_SHORT) .show(); } }
Open MainActivity.java and replace the code with following code snippet to manage the UI.
packagecom.azuresamples.msalandroidapp; importandroid.os.Bundle; importandroidx.annotation.NonNull; importandroidx.appcompat.app.ActionBarDrawerToggle; importandroidx.appcompat.app.AppCompatActivity; importandroidx.appcompat.widget.Toolbar; importandroidx.constraintlayout.widget.ConstraintLayout; importandroidx.core.view.GravityCompat; importandroid.view.MenuItem; importandroid.view.View; importandroidx.drawerlayout.widget.DrawerLayout; importandroidx.fragment.app.Fragment; importandroidx.fragment.app.FragmentTransaction; importcom.google.android.material.navigation.NavigationView; publicclassMainActivityextendsAppCompatActivityimplementsNavigationView.OnNavigationItemSelectedListener, OnFragmentInteractionListener{ enumAppFragment { SingleAccount } privateAppFragmentmCurrentFragment; privateConstraintLayoutmContentMain; @OverrideprotectedvoidonCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mContentMain = findViewById(R.id.content_main); Toolbartoolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); DrawerLayoutdrawer = findViewById(R.id.drawer_layout); NavigationViewnavigationView = findViewById(R.id.nav_view); ActionBarDrawerToggletoggle = newActionBarDrawerToggle( this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawer.addDrawerListener(toggle); toggle.syncState(); navigationView.setNavigationItemSelectedListener(this); //Set default fragmentnavigationView.setCheckedItem(R.id.nav_single_account); setCurrentFragment(AppFragment.SingleAccount); } @OverridepublicbooleanonNavigationItemSelected(finalMenuItemitem) { finalDrawerLayoutdrawer = findViewById(R.id.drawer_layout); drawer.addDrawerListener(newDrawerLayout.DrawerListener() { @OverridepublicvoidonDrawerSlide(@NonNullViewdrawerView, floatslideOffset) { } @OverridepublicvoidonDrawerOpened(@NonNullViewdrawerView) { } @OverridepublicvoidonDrawerClosed(@NonNullViewdrawerView) { // Handle navigation view item clicks here.intid = item.getItemId(); if (id == R.id.nav_single_account) { setCurrentFragment(AppFragment.SingleAccount); } drawer.removeDrawerListener(this); } @OverridepublicvoidonDrawerStateChanged(intnewState) { } }); drawer.closeDrawer(GravityCompat.START); returntrue; } privatevoidsetCurrentFragment(finalAppFragmentnewFragment){ if (newFragment == mCurrentFragment) { return; } mCurrentFragment = newFragment; setHeaderString(mCurrentFragment); displayFragment(mCurrentFragment); } privatevoidsetHeaderString(finalAppFragmentfragment){ switch (fragment) { caseSingleAccount: getSupportActionBar().setTitle("Single Account Mode"); return; } } privatevoiddisplayFragment(finalAppFragmentfragment){ switch (fragment) { caseSingleAccount: attachFragment(newcom.azuresamples.msalandroidapp.SingleAccountModeFragment()); return; } } privatevoidattachFragment(finalFragmentfragment) { getSupportFragmentManager() .beginTransaction() .setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .replace(mContentMain.getId(),fragment) .commit(); } }
Note
Ensure that you update the package name to match your Android project package name.
A layout is a file that defines the visual structure and appearance of a user interface, specifying the arrangement of UI components. It's written in XML. The following XML samples are provided if you would like to model your UI off this tutorial:
In app > src > main> res > layout > activity_main.xml. Replace the content of activity_main.xml with the following code snippet to display buttons and text boxes:
<?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"> <includelayout="@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>
In app > src > main> res > layout > app_bar_main.xml. If you don't have app_bar_main.xml in your folder, create and add the following code snippet:
<?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> <includelayout="@layout/content_main" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
In app > src > main> res > layout > content_main.xml. If you don't have content_main.xml in your folder, create and add the following code snippet:
<?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>
In app > src > main> res > layout > fragment_m_s_graph_request_wrapper.xml. If you don't have fragment_m_s_graph_request_wrapper.xml in your folder, create and add the following code snippet:
<?xml version="1.0" encoding="utf-8"?> <FrameLayoutxmlns: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 --> <TextViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:text="@string/hello_blank_fragment" /> </FrameLayout>
In app > src > main> res > layout > fragment_on_interaction_listener.xml. If you don't have fragment_on_interaction_listener.xml in your folder, create and add the following code snippet:
<?xml version="1.0" encoding="utf-8"?> <FrameLayoutxmlns: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 --> <TextViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:text="@string/hello_blank_fragment" /> </FrameLayout>
In app > src > main> res > layout > fragment_single_account_mode.xml. If you don't have fragment_single_account_mode.xml in your folder, create and add the following code snippet:
<?xml version="1.0" encoding="utf-8"?> <FrameLayoutxmlns: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"> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".SingleAccountModeFragment"> <LinearLayoutandroid: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"> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:paddingTop="5dp"android:paddingBottom="5dp"android:weightSum="10"> <TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="3"android:layout_gravity="center_vertical"android:textStyle="bold"android:text="Scope" /> <LinearLayoutandroid:layout_width="0dp"android:layout_height="wrap_content"android:orientation="vertical"android:layout_weight="7"> <EditTextandroid:id="@+id/scope"android:layout_height="wrap_content"android:layout_width="match_parent"android:text="user.read"android:textSize="12sp" /> <TextViewandroid: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> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:paddingTop="5dp"android:paddingBottom="5dp"android:weightSum="10"> <TextViewandroid: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" /> <LinearLayoutandroid:layout_width="0dp"android:layout_height="wrap_content"android:orientation="vertical"android:layout_weight="7"> <EditTextandroid:id="@+id/msgraph_url"android:layout_height="wrap_content"android:layout_width="match_parent"android:textSize="12sp" /> </LinearLayout> </LinearLayout> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:paddingTop="5dp"android:paddingBottom="5dp"android:weightSum="10"> <TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="3"android:textStyle="bold"android:text="Signed-in user" /> <TextViewandroid:id="@+id/current_user"android:layout_width="0dp"android:layout_height="wrap_content"android:paddingLeft="5dp"android:layout_weight="7"android:text="None" /> </LinearLayout> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:paddingTop="5dp"android:paddingBottom="5dp"android:weightSum="10"> <TextViewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="3"android:textStyle="bold"android:text="Device mode" /> <TextViewandroid:id="@+id/device_mode"android:layout_width="0dp"android:layout_height="wrap_content"android:paddingLeft="5dp"android:layout_weight="7"android:text="None" /> </LinearLayout> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:paddingTop="5dp"android:paddingBottom="5dp"android:weightSum="10"> <Buttonandroid:id="@+id/btn_signIn"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="5"android:gravity="center"android:text="Sign In"/> <Buttonandroid: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> <LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center"android:orientation="horizontal"> <Buttonandroid: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"/> <Buttonandroid: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> <TextViewandroid: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>
In app > src > main> res > layout > nav_header_main.xml. If you don't have nav_header_main.xml in your folder, create and add the following code snippet:
<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns: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"> <ImageViewandroid: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" /> <TextViewandroid: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" /> <TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="MSAL Android" /> </LinearLayout>
In app > src > main> res > menu > activity_main_drawer.xml. If you don't have activity_main_drawer.xml in your folder, create and add the following code snippet:
<?xml version="1.0" encoding="utf-8"?> <menuxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"tools:showIn="navigation_view"> <groupandroid:checkableBehavior="single"> <itemandroid:id="@+id/nav_single_account"android:icon="@drawable/ic_single_account_24dp"android:title="Single Account Mode" /> </group> </menu>
In app > src > main> res > values > dimens.xml. Replace the content of dimens.xml with the following code snippet:
<resources> <dimenname="fab_margin">16dp</dimen> <dimenname="activity_horizontal_margin">16dp</dimen> <dimenname="activity_vertical_margin">16dp</dimen> <dimenname="nav_header_height">176dp</dimen> <dimenname="nav_header_vertical_spacing">8dp</dimen> </resources>
In app > src > main> res > values > colors.xml. Replace the content of colors.xml with the following code snippet:
<?xml version="1.0" encoding="utf-8"?> <resources> <colorname="purple_200">#FFBB86FC</color> <colorname="purple_500">#FF6200EE</color> <colorname="purple_700">#FF3700B3</color> <colorname="teal_200">#FF03DAC5</color> <colorname="teal_700">#FF018786</color> <colorname="black">#FF000000</color> <colorname="white">#FFFFFFFF</color> <colorname="colorPrimary">#008577</color> <colorname="colorPrimaryDark">#00574B</color> <colorname="colorAccent">#D81B60</color> </resources>
In app > src > main> res > values > strings.xml. Replace the content of strings.xml with the following code snippet:
<resources> <stringname="app_name">MSALAndroidapp</string> <stringname="action_settings">Settings</string> <!-- Strings used for fragments for navigation --> <stringname="first_fragment_label">First Fragment</string> <stringname="second_fragment_label">Second Fragment</string> <stringname="nav_header_desc">Navigation header</string> <stringname="navigation_drawer_open">Open navigation drawer</string> <stringname="navigation_drawer_close">Close navigation drawer</string> <stringname="next">Next</string> <stringname="previous">Previous</string> <stringname="hello_first_fragment">Hello first fragment</string> <stringname="hello_second_fragment">Hello second fragment. Arg: %1$s</string> <!-- TODO: Remove or change this placeholder text --> <stringname="hello_blank_fragment">Hello blank fragment</string> </resources>
In app > src > main> res > values > styles.xml. If you don't have styles.xml in your folder, create and add the following code snippet:
<resources> <!-- Base application theme. --> <stylename="AppTheme"parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <itemname="colorPrimary">@color/colorPrimary</item> <itemname="colorPrimaryDark">@color/colorPrimaryDark</item> <itemname="colorAccent">@color/colorAccent</item> </style> <stylename="AppTheme.NoActionBar"> <itemname="windowActionBar">false</item> <itemname="windowNoTitle">true</item> </style> <stylename="AppTheme.AppBarOverlay"parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <stylename="AppTheme.PopupOverlay"parent="ThemeOverlay.AppCompat.Light" /> </resources>
In app > src > main> res > values > themes.xml. Replace the content of themes.xml with the following code snippet:
<resourcesxmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> <stylename="Theme.MSALAndroidapp"parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <!-- Primary brand color. --> <itemname="colorPrimary">@color/purple_500</item> <itemname="colorPrimaryVariant">@color/purple_700</item> <itemname="colorOnPrimary">@color/white</item> <!-- Secondary brand color. --> <itemname="colorSecondary">@color/teal_200</item> <itemname="colorSecondaryVariant">@color/teal_700</item> <itemname="colorOnSecondary">@color/black</item> <!-- Status bar color. --> <itemname="android:statusBarColor"tools:targetApi="21">?attr/colorPrimaryVariant</item> <!-- Customize your theme here. --> </style> <stylename="Theme.MSALAndroidapp.NoActionBar"> <itemname="windowActionBar">false</item> <itemname="windowNoTitle">true</item> </style> <stylename="Theme.MSALAndroidapp.AppBarOverlay"parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <stylename="Theme.MSALAndroidapp.PopupOverlay"parent="ThemeOverlay.AppCompat.Light" /> </resources>
In app > src > main> res > drawable > ic_single_account_24dp.xml. If you don't have ic_single_account_24dp.xml in your folder, create and add the following code snippet:
<vectorxmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <pathandroid: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>
In app > src > main> res > drawable > side_nav_bar.xml. If you don't have side_nav_bar.xml in your folder, create and add the following code snippet:
<shapexmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradientandroid:angle="135"android:centerColor="#009688"android:endColor="#00695C"android:startColor="#4DB6AC"android:type="linear" /> </shape>
In app > src > main> res > drawable. To the folder, add a png Microsoft logo named
microsoft_logo.png
.
Declaring your UI in XML allows you to separate the presentation of your app from the code that controls its behavior. To learn more about Android layout, see Layouts
Build and deploy the app to a test device or emulator. You should be able to sign in and get tokens for Microsoft Entra ID or personal Microsoft accounts.
After you sign in, the app will display the data returned from the Microsoft Graph /me
endpoint.
The first time any user signs into your app, they'll be prompted by Microsoft identity to consent to the permissions requested. Some Microsoft Entra tenants have disabled user consent, which requires admins to consent on behalf of all users. To support this scenario, you'll either need to create your own tenant or receive admin consent.
When no longer needed, delete the app object that you created in the Register your application step.
[!INCLUDE Help and support]
To explore more complex scenarios, see a completed working code sample on GitHub.
For more information about building mobile apps that call protected web APIs in our multi-part scenario series, see: