From c5130fb438d27c1d7754902f8ee171ce8c1ee04a Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 12:08:28 -0500 Subject: [PATCH 01/16] Renamed demo directory --- {demo => user-auth}/README.md | 0 {demo => user-auth}/RegisterAppForUserAuth.ps1 | 0 {demo => user-auth}/UpdateAppForAppOnlyAuth.ps1 | 0 {demo => user-auth}/graphtutorial/.gitattributes | 0 {demo => user-auth}/graphtutorial/.gitignore | 0 {demo => user-auth}/graphtutorial/app/build.gradle | 0 .../app/src/main/java/graphtutorial/App.java | 0 .../app/src/main/java/graphtutorial/Graph.java | 0 .../graphtutorial/oAuth.properties.example | 0 .../graphtutorial/gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 {demo => user-auth}/graphtutorial/gradlew | 0 {demo => user-auth}/graphtutorial/gradlew.bat | 0 {demo => user-auth}/graphtutorial/settings.gradle | 0 {demo => user-auth}/version | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename {demo => user-auth}/README.md (100%) rename {demo => user-auth}/RegisterAppForUserAuth.ps1 (100%) rename {demo => user-auth}/UpdateAppForAppOnlyAuth.ps1 (100%) rename {demo => user-auth}/graphtutorial/.gitattributes (100%) rename {demo => user-auth}/graphtutorial/.gitignore (100%) rename {demo => user-auth}/graphtutorial/app/build.gradle (100%) rename {demo => user-auth}/graphtutorial/app/src/main/java/graphtutorial/App.java (100%) rename {demo => user-auth}/graphtutorial/app/src/main/java/graphtutorial/Graph.java (100%) rename {demo => user-auth}/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example (100%) rename {demo => user-auth}/graphtutorial/gradle/wrapper/gradle-wrapper.jar (100%) rename {demo => user-auth}/graphtutorial/gradle/wrapper/gradle-wrapper.properties (100%) rename {demo => user-auth}/graphtutorial/gradlew (100%) rename {demo => user-auth}/graphtutorial/gradlew.bat (100%) rename {demo => user-auth}/graphtutorial/settings.gradle (100%) rename {demo => user-auth}/version (100%) diff --git a/demo/README.md b/user-auth/README.md similarity index 100% rename from demo/README.md rename to user-auth/README.md diff --git a/demo/RegisterAppForUserAuth.ps1 b/user-auth/RegisterAppForUserAuth.ps1 similarity index 100% rename from demo/RegisterAppForUserAuth.ps1 rename to user-auth/RegisterAppForUserAuth.ps1 diff --git a/demo/UpdateAppForAppOnlyAuth.ps1 b/user-auth/UpdateAppForAppOnlyAuth.ps1 similarity index 100% rename from demo/UpdateAppForAppOnlyAuth.ps1 rename to user-auth/UpdateAppForAppOnlyAuth.ps1 diff --git a/demo/graphtutorial/.gitattributes b/user-auth/graphtutorial/.gitattributes similarity index 100% rename from demo/graphtutorial/.gitattributes rename to user-auth/graphtutorial/.gitattributes diff --git a/demo/graphtutorial/.gitignore b/user-auth/graphtutorial/.gitignore similarity index 100% rename from demo/graphtutorial/.gitignore rename to user-auth/graphtutorial/.gitignore diff --git a/demo/graphtutorial/app/build.gradle b/user-auth/graphtutorial/app/build.gradle similarity index 100% rename from demo/graphtutorial/app/build.gradle rename to user-auth/graphtutorial/app/build.gradle diff --git a/demo/graphtutorial/app/src/main/java/graphtutorial/App.java b/user-auth/graphtutorial/app/src/main/java/graphtutorial/App.java similarity index 100% rename from demo/graphtutorial/app/src/main/java/graphtutorial/App.java rename to user-auth/graphtutorial/app/src/main/java/graphtutorial/App.java diff --git a/demo/graphtutorial/app/src/main/java/graphtutorial/Graph.java b/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java similarity index 100% rename from demo/graphtutorial/app/src/main/java/graphtutorial/Graph.java rename to user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java diff --git a/demo/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example b/user-auth/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example similarity index 100% rename from demo/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example rename to user-auth/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example diff --git a/demo/graphtutorial/gradle/wrapper/gradle-wrapper.jar b/user-auth/graphtutorial/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from demo/graphtutorial/gradle/wrapper/gradle-wrapper.jar rename to user-auth/graphtutorial/gradle/wrapper/gradle-wrapper.jar diff --git a/demo/graphtutorial/gradle/wrapper/gradle-wrapper.properties b/user-auth/graphtutorial/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from demo/graphtutorial/gradle/wrapper/gradle-wrapper.properties rename to user-auth/graphtutorial/gradle/wrapper/gradle-wrapper.properties diff --git a/demo/graphtutorial/gradlew b/user-auth/graphtutorial/gradlew similarity index 100% rename from demo/graphtutorial/gradlew rename to user-auth/graphtutorial/gradlew diff --git a/demo/graphtutorial/gradlew.bat b/user-auth/graphtutorial/gradlew.bat similarity index 100% rename from demo/graphtutorial/gradlew.bat rename to user-auth/graphtutorial/gradlew.bat diff --git a/demo/graphtutorial/settings.gradle b/user-auth/graphtutorial/settings.gradle similarity index 100% rename from demo/graphtutorial/settings.gradle rename to user-auth/graphtutorial/settings.gradle diff --git a/demo/version b/user-auth/version similarity index 100% rename from demo/version rename to user-auth/version From 2bda9583d5046ed09fa2e36eebe72ab2c56b8dc3 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 12:21:12 -0500 Subject: [PATCH 02/16] Copied project for app-only --- app-auth/README.md | 125 ++++++++++ app-auth/RegisterAppForAppOnlyAuth.ps1 | 113 +++++++++ app-auth/graphapponlytutorial/.gitattributes | 6 + app-auth/graphapponlytutorial/.gitignore | 5 + .../graphapponlytutorial/app/build.gradle | 40 +++ .../main/java/graphapponlytutorial/App.java | 209 ++++++++++++++++ .../main/java/graphapponlytutorial/Graph.java | 201 +++++++++++++++ .../oAuth.properties.example | 5 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + app-auth/graphapponlytutorial/gradlew | 234 ++++++++++++++++++ app-auth/graphapponlytutorial/gradlew.bat | 89 +++++++ app-auth/graphapponlytutorial/settings.gradle | 11 + 13 files changed, 1043 insertions(+) create mode 100644 app-auth/README.md create mode 100644 app-auth/RegisterAppForAppOnlyAuth.ps1 create mode 100644 app-auth/graphapponlytutorial/.gitattributes create mode 100644 app-auth/graphapponlytutorial/.gitignore create mode 100644 app-auth/graphapponlytutorial/app/build.gradle create mode 100644 app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java create mode 100644 app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java create mode 100644 app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example create mode 100644 app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.jar create mode 100644 app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.properties create mode 100644 app-auth/graphapponlytutorial/gradlew create mode 100644 app-auth/graphapponlytutorial/gradlew.bat create mode 100644 app-auth/graphapponlytutorial/settings.gradle diff --git a/app-auth/README.md b/app-auth/README.md new file mode 100644 index 0000000..bd9d8a9 --- /dev/null +++ b/app-auth/README.md @@ -0,0 +1,125 @@ +# How to run the completed project + +## Prerequisites + +To run the completed project in this folder, you need the following: + +- [Java SE Development Kit (JDK)](https://java.com/en/download/faq/develop.xml) and [Gradle](https://gradle.org/) installed on your development machine. (**Note:** This tutorial was written with OpenJDK version 17.0.2 and Gradle 7.4.2. The steps in this guide may work with other versions, but that has not been tested.) +- A Microsoft work or school account. + +If you don't have a Microsoft account, you can [sign up for the Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) to get a free Microsoft 365 subscription. + +## Register an application + +You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/get-started). + +**NOTE:** If you downloaded this code from [https://developer.microsoft.com/graph/quick-start](https://developer.microsoft.com/graph/quick-start), an app registration has already been created for you. However, if you want to use the app-only portion of this sample, you will need to modify the app registration as specified in [Configure app-only auth (AAD admin center)](#configure-app-only-auth-aad-admin-center) or [Configure app-only auth (PowerShell)](#configure-app-only-auth-powershell). + +### Azure Active Directory admin center + +1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com) and login using a **personal account** (aka: Microsoft Account) or **Work or School Account**. + +1. Select **Azure Active Directory** in the left-hand navigation, then select **App registrations** under **Manage**. + +1. Select **New registration**. Enter a name for your application, for example, `Java Graph Tutorial`. + +1. Set **Supported account types** as desired. The options are: + + | Option | Who can sign in? | + |--------|------------------| + | **Accounts in this organizational directory only** | Only users in your Microsoft 365 organization | + | **Accounts in any organizational directory** | Users in any Microsoft 365 organization (work or school accounts) | + | **Accounts in any organizational directory ... and personal Microsoft accounts** | Users in any Microsoft 365 organization (work or school accounts) and personal Microsoft accounts | + +1. Leave **Redirect URI** empty. + +1. Select **Register**. On the application's **Overview** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step. If you chose **Accounts in this organizational directory only** for **Supported account types**, also copy the **Directory (tenant) ID** and save it. + +1. Select **Authentication** under **Manage**. Locate the **Advanced settings** section and change the **Allow public client flows** toggle to **Yes**, then choose **Save**. + +#### Configure app-only auth (AAD admin center) + +> **Note:** This section requires a work/school account with the Global administrator role. You only need to complete these steps if you plan on using the app-only portions of this sample. + +1. Select **API permissions** under **Manage**. + +1. Remove the default **User.Read** permission under **Configured permissions** by selecting the ellipses (**...**) in its row and selecting **Remove permission**. + +1. Select **Add a permission**, then **Microsoft Graph**. + +1. Select **Application permissions**. + +1. Select **User.Read.All**, then select **Add permissions**. + +1. Select **Grant admin consent for...**, then select **Yes** to provide admin consent for the selected permission. + +1. Select **Certificates and secrets** under **Manage**, then select **New client secret**. + +1. Enter a description, choose a duration, and select **Add**. + +1. Copy the secret from the **Value** column, you will need it in the next steps. + +### PowerShell + +To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/installation) for installation instructions. + +1. Open PowerShell and run the [RegisterAppForUserAuth.ps1](RegisterAppForUserAuth.ps1) file with the following command, replacing *<audience-value>* with the desired value (see table below). + + > **Note:** The RegisterAppForUserAuth.ps1 script requires a work/school account with the Application administrator, Cloud application administrator, or Global administrator role. + + ```powershell + .\RegisterAppForUserAuth.ps1 -AppName "Java Graph Tutorial" -SignInAudience + ``` + + | SignInAudience value | Who can sign in? | + |----------------------|------------------| + | `AzureADMyOrg` | Only users in your Microsoft 365 organization | + | `AzureADMultipleOrgs` | Users in any Microsoft 365 organization (work or school accounts) | + | `AzureADandPersonalMicrosoftAccount` | Users in any Microsoft 365 organization (work or school accounts) and personal Microsoft accounts | + | `PersonalMicrosoftAccount` | Only personal Microsoft accounts | + +1. Copy the **Client ID** and **Auth tenant** values from the script output. You will need these values in the next step. + + ```powershell + SUCCESS + Client ID: 2fb1652f-a9a0-4db9-b220-b224b8d9d38b + Auth tenant: common + ``` + +#### Configure app-only auth (PowerShell) + +> **Note:** This section requires a work/school account with the Global administrator role. You only need to complete these steps if you plan on using the app-only portions of this sample. + +1. Run the [UpdateAppForAppOnlyAuth.ps1](UpdateAppForAppOnlyAuth.ps1) file with the following command, replacing *<your-client-id>* with your client ID. + + ```powershell + .\UpdateAppForAppOnlyAuth.ps1 -AppId -GraphScopes "User.Read.All" + ``` + +1. Copy the **Tenant ID** and **Client secret** values from the script output. You will need these values in the next step. + + ```powershell + SUCCESS + Tenant ID: a795ad0f-7d82-4a3b-a2c0-0713ec72ade7 + Client secret: 2jv7Q~8eiOd_QafJ..... + Secret expires: 2/16/2024 9:32:09 PM + ``` + +## Configure the sample + +1. Rename [oAuth.properties.example](./graphtutorial/app/src/main/resources/oAuth.properties.example) to `oauthProperties` and update the values according to the following table. + + | Setting | Value | + |---------|-------| + | `app.clientId` | The client ID of your app registration | + | `app.clientSecret` | The client secret (only needed if doing app-only) | + | `app.tenantId` | The tenant ID of your organization (only needed if doing app-only) | + | `app.authTenant` | If you chose the option to only allow users in your organization to sign in, change this value to your tenant ID. Otherwise leave as `common`. | + +## Build and run the sample + +In your command-line interface (CLI), navigate to the project directory and run the following command. + +```Shell +./gradlew --console plain run +``` diff --git a/app-auth/RegisterAppForAppOnlyAuth.ps1 b/app-auth/RegisterAppForAppOnlyAuth.ps1 new file mode 100644 index 0000000..09ce5e0 --- /dev/null +++ b/app-auth/RegisterAppForAppOnlyAuth.ps1 @@ -0,0 +1,113 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +# +param( + [Parameter(Mandatory=$true, + HelpMessage="The friendly name of the app registration")] + [String] + $AppName, + + [Parameter(Mandatory=$true, + HelpMessage="The application permission scopes to configure on the app registration")] + [String[]] + $GraphScopes, + + [Parameter(Mandatory=$false)] + [Switch] + $StayConnected = $false +) + +$graphAppId = "00000003-0000-0000-c000-000000000000" + +# Requires an admin +Connect-MgGraph -Scopes "Application.ReadWrite.All User.Read" -UseDeviceAuthentication -ErrorAction Stop + +# Get context for access to tenant ID +$context = Get-MgContext -ErrorAction Stop +$authTenant = $context.TenantId + +# Create app registration +$appRegistration = New-MgApplication -DisplayName $AppName -SignInAudience "AzureADMyOrg" -ErrorAction Stop +Write-Host -ForegroundColor Cyan "App registration created with app ID" $appRegistration.AppId + +# Create corresponding service principal +$appServicePrincipal = New-MgServicePrincipal -AppId $appRegistration.AppId -ErrorAction SilentlyContinue ` + -ErrorVariable SPError +if ($SPError) +{ + Write-Host -ForegroundColor Red "A service principal for the app could not be created." + Write-Host -ForegroundColor Red $SPError + Exit +} + +Write-Host -ForegroundColor Cyan "Service principal created" + +# Lookup available Graph application permissions +$graphServicePrincipal = Get-MgServicePrincipal -Filter ("appId eq '" + $graphAppId + "'") -ErrorAction Stop +$graphAppPermissions = $graphServicePrincipal.AppRoles + +$resourceAccess = @() + +foreach($scope in $GraphScopes) +{ + $permission = $graphAppPermissions | Where-Object { $_.Value -eq $scope } + if ($permission) + { + $resourceAccess += @{ Id = $permission.Id; Type = "Role"} + } + else + { + Write-Host -ForegroundColor Red "Invalid scope:" $scope + Exit + } +} + +# Add the permissions to required resource access +Update-MgApplication -ApplicationId $appRegistration.Id -RequiredResourceAccess ` + @{ ResourceAppId = $graphAppId; ResourceAccess = $resourceAccess } -ErrorAction Stop +Write-Host -ForegroundColor Cyan "Added application permissions to app registration" + +# Add admin consent +foreach ($appRole in $resourceAccess) +{ + $appServicePrincipal + New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $appServicePrincipal.Id ` + -PrincipalId $appServicePrincipal.Id -ResourceId $graphServicePrincipal.Id ` + -AppRoleId $appRole.Id -ErrorAction SilentlyContinue -ErrorVariable SPError | Out-Null + if ($SPError) + { + Write-Host -ForegroundColor Red "Admin consent for one of the requested scopes could not be added." + Write-Host -ForegroundColor Red $SPError + Exit + } +} +Write-Host -ForegroundColor Cyan "Added admin consent" + +# Add a client secret +$clientSecret = Add-MgApplicationPassword -ApplicationId $appRegistration.Id -PasswordCredential ` + @{ DisplayName = "Added by PowerShell" } -ErrorAction Stop + +Write-Host +Write-Host -ForegroundColor Green "SUCCESS" +Write-Host -ForegroundColor Cyan -NoNewline "Client ID: " +Write-Host -ForegroundColor Yellow $appRegistration.AppId +Write-Host -ForegroundColor Cyan -NoNewline "Tenant ID: " +Write-Host -ForegroundColor Yellow $authTenant +Write-Host -ForegroundColor Cyan -NoNewline "Client secret: " +Write-Host -ForegroundColor Yellow $clientSecret.SecretText +Write-Host -ForegroundColor Cyan -NoNewline "Secret expires: " +Write-Host -ForegroundColor Yellow $clientSecret.EndDateTime + +if ($StayConnected -eq $false) +{ + Disconnect-MgGraph + Write-Host "Disconnected from Microsoft Graph" +} +else +{ + Write-Host + Write-Host -ForegroundColor Yellow ` + "The connection to Microsoft Graph is still active. To disconnect, use Disconnect-MgGraph" +} +# diff --git a/app-auth/graphapponlytutorial/.gitattributes b/app-auth/graphapponlytutorial/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/app-auth/graphapponlytutorial/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/app-auth/graphapponlytutorial/.gitignore b/app-auth/graphapponlytutorial/.gitignore new file mode 100644 index 0000000..1b6985c --- /dev/null +++ b/app-auth/graphapponlytutorial/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/app-auth/graphapponlytutorial/app/build.gradle b/app-auth/graphapponlytutorial/app/build.gradle new file mode 100644 index 0000000..6b8adf3 --- /dev/null +++ b/app-auth/graphapponlytutorial/app/build.gradle @@ -0,0 +1,40 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java application project to get you started. + * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle + * User Manual available at https://docs.gradle.org/7.4.2/userguide/building_java_projects.html + */ + +plugins { + // Apply the application plugin to add support for building a CLI application in Java. + id 'application' +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +// +dependencies { + // Use JUnit test framework. + testImplementation 'junit:junit:4.13.2' + + // This dependency is used by the application. + implementation 'com.google.guava:guava:31.1-jre' + implementation 'com.azure:azure-identity:1.6.1' + implementation 'com.microsoft.graph:microsoft-graph:5.40.0' +} +// + +application { + // Define the main class for the application. + mainClass = 'graphapponlytutorial.App' +} + +// +run { + standardInput = System.in +} +// diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java new file mode 100644 index 0000000..a403457 --- /dev/null +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +// +package graphapponlytutorial; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.InputMismatchException; +import java.util.Properties; +import java.util.Scanner; + +import com.microsoft.graph.models.Message; +import com.microsoft.graph.models.User; +import com.microsoft.graph.requests.MessageCollectionPage; +import com.microsoft.graph.requests.UserCollectionPage; +// + +public class App { + // + public static void main(String[] args) { + System.out.println("Java Graph Tutorial"); + System.out.println(); + + final Properties oAuthProperties = new Properties(); + try { + oAuthProperties.load(App.class.getResourceAsStream("oAuth.properties")); + } catch (IOException e) { + System.out.println("Unable to read OAuth configuration. Make sure you have a properly formatted oAuth.properties file. See README for details."); + return; + } + + initializeGraph(oAuthProperties); + + greetUser(); + + Scanner input = new Scanner(System.in); + + int choice = -1; + + while (choice != 0) { + System.out.println("Please choose one of the following options:"); + System.out.println("0. Exit"); + System.out.println("1. Display access token"); + System.out.println("2. List my inbox"); + System.out.println("3. Send mail"); + System.out.println("4. List users (required app-only)"); + System.out.println("5. Make a Graph call"); + + try { + choice = input.nextInt(); + } catch (InputMismatchException ex) { + // Skip over non-integer input + } + + input.nextLine(); + + // Process user choice + switch(choice) { + case 0: + // Exit the program + System.out.println("Goodbye..."); + break; + case 1: + // Display access token + displayAccessToken(); + break; + case 2: + // List emails from user's inbox + listInbox(); + break; + case 3: + // Send an email message + sendMail(); + break; + case 4: + // List users + listUsers(); + break; + case 5: + // Run any Graph code + makeGraphCall(); + break; + default: + System.out.println("Invalid choice"); + } + } + + input.close(); + } + // + + // + private static void initializeGraph(Properties properties) { + try { + Graph.initializeGraphForUserAuth(properties, + challenge -> System.out.println(challenge.getMessage())); + } catch (Exception e) + { + System.out.println("Error initializing Graph for user auth"); + System.out.println(e.getMessage()); + } + } + // + + // + private static void greetUser() { + try { + final User user = Graph.getUser(); + // For Work/school accounts, email is in mail property + // Personal accounts, email is in userPrincipalName + final String email = user.mail == null ? user.userPrincipalName : user.mail; + System.out.println("Hello, " + user.displayName + "!"); + System.out.println("Email: " + email); + } catch (Exception e) { + System.out.println("Error getting user"); + System.out.println(e.getMessage()); + } + } + // + + // + private static void displayAccessToken() { + try { + final String accessToken = Graph.getUserToken(); + System.out.println("Access token: " + accessToken); + } catch (Exception e) { + System.out.println("Error getting access token"); + System.out.println(e.getMessage()); + } + } + // + + // + private static void listInbox() { + try { + final MessageCollectionPage messages = Graph.getInbox(); + + // Output each message's details + for (Message message: messages.getCurrentPage()) { + System.out.println("Message: " + message.subject); + System.out.println(" From: " + message.from.emailAddress.name); + System.out.println(" Status: " + (message.isRead ? "Read" : "Unread")); + System.out.println(" Received: " + message.receivedDateTime + // Values are returned in UTC, convert to local time zone + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime() + .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT))); + } + + final Boolean moreMessagesAvailable = messages.getNextPage() != null; + System.out.println("\nMore messages available? " + moreMessagesAvailable); + } catch (Exception e) { + System.out.println("Error getting inbox"); + System.out.println(e.getMessage()); + } + } + // + + // + private static void sendMail() { + try { + // Send mail to the signed-in user + // Get the user for their email address + final User user = Graph.getUser(); + final String email = user.mail == null ? user.userPrincipalName : user.mail; + + Graph.sendMail("Testing Microsoft Graph", "Hello world!", email); + System.out.println("\nMail sent."); + } catch (Exception e) { + System.out.println("Error sending mail"); + System.out.println(e.getMessage()); + } + } + // + + // + private static void listUsers() { + try { + final UserCollectionPage users = Graph.getUsers(); + + // Output each user's details + for (User user: users.getCurrentPage()) { + System.out.println("User: " + user.displayName); + System.out.println(" ID: " + user.id); + System.out.println(" Email: " + user.mail); + } + + final Boolean moreUsersAvailable = users.getNextPage() != null; + System.out.println("\nMore users available? " + moreUsersAvailable); + } catch (Exception e) { + System.out.println("Error getting users"); + System.out.println(e.getMessage()); + } + } + // + + // + private static void makeGraphCall() { + try { + Graph.makeGraphCall(); + } catch (Exception e) { + System.out.println("Error making Graph call"); + System.out.println(e.getMessage()); + } + } + // +} diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java new file mode 100644 index 0000000..8faeea9 --- /dev/null +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +// +package graphapponlytutorial; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.function.Consumer; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.DeviceCodeCredential; +import com.azure.identity.DeviceCodeCredentialBuilder; +import com.azure.identity.DeviceCodeInfo; +import com.microsoft.graph.authentication.TokenCredentialAuthProvider; +import com.microsoft.graph.models.BodyType; +import com.microsoft.graph.models.EmailAddress; +import com.microsoft.graph.models.ItemBody; +import com.microsoft.graph.models.Message; +import com.microsoft.graph.models.Recipient; +import com.microsoft.graph.models.User; +import com.microsoft.graph.models.UserSendMailParameterSet; +import com.microsoft.graph.requests.GraphServiceClient; +import com.microsoft.graph.requests.MessageCollectionPage; +import com.microsoft.graph.requests.UserCollectionPage; + +import okhttp3.Request; +// + +public class Graph { + // + private static Properties _properties; + private static DeviceCodeCredential _deviceCodeCredential; + private static GraphServiceClient _userClient; + + public static void initializeGraphForUserAuth(Properties properties, Consumer challenge) throws Exception { + // Ensure properties isn't null + if (properties == null) { + throw new Exception("Properties cannot be null"); + } + + _properties = properties; + + final String clientId = properties.getProperty("app.clientId"); + final String authTenantId = properties.getProperty("app.authTenant"); + final List graphUserScopes = Arrays + .asList(properties.getProperty("app.graphUserScopes").split(",")); + + _deviceCodeCredential = new DeviceCodeCredentialBuilder() + .clientId(clientId) + .tenantId(authTenantId) + .challengeConsumer(challenge) + .build(); + + final TokenCredentialAuthProvider authProvider = + new TokenCredentialAuthProvider(graphUserScopes, _deviceCodeCredential); + + _userClient = GraphServiceClient.builder() + .authenticationProvider(authProvider) + .buildClient(); + } + // + + // + public static String getUserToken() throws Exception { + // Ensure credential isn't null + if (_deviceCodeCredential == null) { + throw new Exception("Graph has not been initialized for user auth"); + } + + final String[] graphUserScopes = _properties.getProperty("app.graphUserScopes").split(","); + + final TokenRequestContext context = new TokenRequestContext(); + context.addScopes(graphUserScopes); + + final AccessToken token = _deviceCodeCredential.getToken(context).block(); + return token.getToken(); + } + // + + // + public static User getUser() throws Exception { + // Ensure client isn't null + if (_userClient == null) { + throw new Exception("Graph has not been initialized for user auth"); + } + + return _userClient.me() + .buildRequest() + .select("displayName,mail,userPrincipalName") + .get(); + } + // + + // + public static MessageCollectionPage getInbox() throws Exception { + // Ensure client isn't null + if (_userClient == null) { + throw new Exception("Graph has not been initialized for user auth"); + } + + return _userClient.me() + .mailFolders("inbox") + .messages() + .buildRequest() + .select("from,isRead,receivedDateTime,subject") + .top(25) + .orderBy("receivedDateTime DESC") + .get(); + } + // + + // + public static void sendMail(String subject, String body, String recipient) throws Exception { + // Ensure client isn't null + if (_userClient == null) { + throw new Exception("Graph has not been initialized for user auth"); + } + + // Create a new message + final Message message = new Message(); + message.subject = subject; + message.body = new ItemBody(); + message.body.content = body; + message.body.contentType = BodyType.TEXT; + + final Recipient toRecipient = new Recipient(); + toRecipient.emailAddress = new EmailAddress(); + toRecipient.emailAddress.address = recipient; + message.toRecipients = List.of(toRecipient); + + // Send the message + _userClient.me() + .sendMail(UserSendMailParameterSet.newBuilder() + .withMessage(message) + .build()) + .buildRequest() + .post(); + } + // + + // + private static ClientSecretCredential _clientSecretCredential; + private static GraphServiceClient _appClient; + + private static void ensureGraphForAppOnlyAuth() throws Exception { + // Ensure _properties isn't null + if (_properties == null) { + throw new Exception("Properties cannot be null"); + } + + if (_clientSecretCredential == null) { + final String clientId = _properties.getProperty("app.clientId"); + final String tenantId = _properties.getProperty("app.tenantId"); + final String clientSecret = _properties.getProperty("app.clientSecret"); + + _clientSecretCredential = new ClientSecretCredentialBuilder() + .clientId(clientId) + .tenantId(tenantId) + .clientSecret(clientSecret) + .build(); + } + + if (_appClient == null) { + final TokenCredentialAuthProvider authProvider = + new TokenCredentialAuthProvider( + List.of("https://graph.microsoft.com/.default"), _clientSecretCredential); + + _appClient = GraphServiceClient.builder() + .authenticationProvider(authProvider) + .buildClient(); + } + } + // + + // + public static UserCollectionPage getUsers() throws Exception { + ensureGraphForAppOnlyAuth(); + + return _appClient.users() + .buildRequest() + .select("displayName,id,mail") + .top(25) + .orderBy("displayName") + .get(); + } + // + + // + public static void makeGraphCall() { + // INSERT YOUR CODE HERE + // Note: if using _appClient, be sure to call ensureGraphForAppOnlyAuth + // before using it. + // ensureGraphForAppOnlyAuth(); + } + // +} diff --git a/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example b/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example new file mode 100644 index 0000000..4b54e97 --- /dev/null +++ b/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example @@ -0,0 +1,5 @@ +app.clientId=YOUR_CLIENT_ID_HERE +app.clientSecret=YOUR_CLIENT_SECRET_HERE_IF_USING_APP_ONLY +app.tenantId=YOUR_TENANT_ID_HERE_IF_USING_APP_ONLY +app.authTenant=common +app.graphUserScopes=user.read,mail.read,mail.send diff --git a/app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.jar b/app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.properties b/app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa991fc --- /dev/null +++ b/app-auth/graphapponlytutorial/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/app-auth/graphapponlytutorial/gradlew b/app-auth/graphapponlytutorial/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/app-auth/graphapponlytutorial/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/app-auth/graphapponlytutorial/gradlew.bat b/app-auth/graphapponlytutorial/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/app-auth/graphapponlytutorial/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/app-auth/graphapponlytutorial/settings.gradle b/app-auth/graphapponlytutorial/settings.gradle new file mode 100644 index 0000000..e035f8a --- /dev/null +++ b/app-auth/graphapponlytutorial/settings.gradle @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.4.2/userguide/multi_project_builds.html + */ + +rootProject.name = 'graphapponlytutorial' +include('app') From 80e6cfddb1e6d1156dd84f943465047043331fcd Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:08:34 -0500 Subject: [PATCH 03/16] Update user auth sample --- user-auth/UpdateAppForAppOnlyAuth.ps1 | 98 ------------------- .../app/src/main/java/graphtutorial/App.java | 29 +----- .../src/main/java/graphtutorial/Graph.java | 54 +--------- .../graphtutorial/oAuth.properties.example | 4 +- user-auth/version | 2 +- 5 files changed, 5 insertions(+), 182 deletions(-) delete mode 100644 user-auth/UpdateAppForAppOnlyAuth.ps1 diff --git a/user-auth/UpdateAppForAppOnlyAuth.ps1 b/user-auth/UpdateAppForAppOnlyAuth.ps1 deleted file mode 100644 index d6833c6..0000000 --- a/user-auth/UpdateAppForAppOnlyAuth.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT license. - -# -param( - [Parameter(Mandatory=$true, - HelpMessage="The app ID of the app registration")] - [String] - $AppId, - - [Parameter(Mandatory=$true, - HelpMessage="The application permission scopes to configure on the app registration")] - [String[]] - $GraphScopes, - - [Parameter(Mandatory=$false)] - [Switch] - $StayConnected = $false -) - -$graphAppId = "00000003-0000-0000-c000-000000000000" - -# Requires an admin -Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All User.Read" ` - -UseDeviceAuthentication -ErrorAction Stop - -# Get context for access to tenant ID -$context = Get-MgContext -ErrorAction Stop - -# Get the application and service principal -$appRegistration = Get-MgApplication -Filter ("appId eq '" + $AppId +"'") -ErrorAction Stop -$appServicePrincipal = Get-MgServicePrincipal -Filter ("appId eq '" + $AppId + "'") -ErrorAction Stop - -# Lookup available Graph application permissions -$graphServicePrincipal = Get-MgServicePrincipal -Filter ("appId eq '" + $graphAppId + "'") -ErrorAction Stop -$graphAppPermissions = $graphServicePrincipal.AppRoles - -$resourceAccess = @() - -foreach($scope in $GraphScopes) -{ - $permission = $graphAppPermissions | Where-Object { $_.Value -eq $scope } - if ($permission) - { - $resourceAccess += @{ Id = $permission.Id; Type = "Role"} - } - else - { - Write-Host -ForegroundColor Red "Invalid scope:" $scope - Exit - } -} - -# Add the permissions to required resource access -Update-MgApplication -ApplicationId $appRegistration.Id -RequiredResourceAccess ` - @{ ResourceAppId = $graphAppId; ResourceAccess = $resourceAccess } -ErrorAction Stop -Write-Host -ForegroundColor Cyan "Added application permissions to app registration" - -# Add admin consent -foreach ($appRole in $resourceAccess) -{ - New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $appServicePrincipal.Id ` - -PrincipalId $appServicePrincipal.Id -ResourceId $graphServicePrincipal.Id ` - -AppRoleId $appRole.Id -ErrorAction SilentlyContinue -ErrorVariable SPError | Out-Null - if ($SPError) - { - Write-Host -ForegroundColor Red "Admin consent for one of the requested scopes could not be added." - Write-Host -ForegroundColor Red $SPError - Exit - } -} -Write-Host -ForegroundColor Cyan "Added admin consent" - -# Add a client secret -$clientSecret = Add-MgApplicationPassword -ApplicationId $appRegistration.Id -PasswordCredential ` - @{ DisplayName = "Added by PowerShell" } -ErrorAction Stop - -Write-Host -Write-Host -ForegroundColor Green "SUCCESS" -Write-Host -ForegroundColor Cyan -NoNewline "Tenant ID: " -Write-Host -ForegroundColor Yellow $context.TenantId -Write-Host -ForegroundColor Cyan -NoNewline "Client secret: " -Write-Host -ForegroundColor Yellow $clientSecret.SecretText -Write-Host -ForegroundColor Cyan -NoNewline "Secret expires: " -Write-Host -ForegroundColor Yellow $clientSecret.EndDateTime - -if ($StayConnected -eq $false) -{ - Disconnect-MgGraph - Write-Host "Disconnected from Microsoft Graph" -} -else -{ - Write-Host - Write-Host -ForegroundColor Yellow ` - "The connection to Microsoft Graph is still active. To disconnect, use Disconnect-MgGraph" -} -# diff --git a/user-auth/graphtutorial/app/src/main/java/graphtutorial/App.java b/user-auth/graphtutorial/app/src/main/java/graphtutorial/App.java index f5cc1a7..c70ff84 100644 --- a/user-auth/graphtutorial/app/src/main/java/graphtutorial/App.java +++ b/user-auth/graphtutorial/app/src/main/java/graphtutorial/App.java @@ -15,7 +15,6 @@ import com.microsoft.graph.models.Message; import com.microsoft.graph.models.User; import com.microsoft.graph.requests.MessageCollectionPage; -import com.microsoft.graph.requests.UserCollectionPage; // public class App { @@ -46,8 +45,7 @@ public static void main(String[] args) { System.out.println("1. Display access token"); System.out.println("2. List my inbox"); System.out.println("3. Send mail"); - System.out.println("4. List users (required app-only)"); - System.out.println("5. Make a Graph call"); + System.out.println("4. Make a Graph call"); try { choice = input.nextInt(); @@ -76,10 +74,6 @@ public static void main(String[] args) { sendMail(); break; case 4: - // List users - listUsers(); - break; - case 5: // Run any Graph code makeGraphCall(); break; @@ -175,27 +169,6 @@ private static void sendMail() { } // - // - private static void listUsers() { - try { - final UserCollectionPage users = Graph.getUsers(); - - // Output each user's details - for (User user: users.getCurrentPage()) { - System.out.println("User: " + user.displayName); - System.out.println(" ID: " + user.id); - System.out.println(" Email: " + user.mail); - } - - final Boolean moreUsersAvailable = users.getNextPage() != null; - System.out.println("\nMore users available? " + moreUsersAvailable); - } catch (Exception e) { - System.out.println("Error getting users"); - System.out.println(e.getMessage()); - } - } - // - // private static void makeGraphCall() { try { diff --git a/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java b/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java index 59e0907..464eb9f 100644 --- a/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java +++ b/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java @@ -11,8 +11,6 @@ import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenRequestContext; -import com.azure.identity.ClientSecretCredential; -import com.azure.identity.ClientSecretCredentialBuilder; import com.azure.identity.DeviceCodeCredential; import com.azure.identity.DeviceCodeCredentialBuilder; import com.azure.identity.DeviceCodeInfo; @@ -26,7 +24,6 @@ import com.microsoft.graph.models.UserSendMailParameterSet; import com.microsoft.graph.requests.GraphServiceClient; import com.microsoft.graph.requests.MessageCollectionPage; -import com.microsoft.graph.requests.UserCollectionPage; import okhttp3.Request; // @@ -46,13 +43,13 @@ public static void initializeGraphForUserAuth(Properties properties, Consumer graphUserScopes = Arrays .asList(properties.getProperty("app.graphUserScopes").split(",")); _deviceCodeCredential = new DeviceCodeCredentialBuilder() .clientId(clientId) - .tenantId(authTenantId) + .tenantId(tenantId) .challengeConsumer(challenge) .build(); @@ -143,53 +140,6 @@ public static void sendMail(String subject, String body, String recipient) throw } // - // - private static ClientSecretCredential _clientSecretCredential; - private static GraphServiceClient _appClient; - - private static void ensureGraphForAppOnlyAuth() throws Exception { - // Ensure _properties isn't null - if (_properties == null) { - throw new Exception("Properties cannot be null"); - } - - if (_clientSecretCredential == null) { - final String clientId = _properties.getProperty("app.clientId"); - final String tenantId = _properties.getProperty("app.tenantId"); - final String clientSecret = _properties.getProperty("app.clientSecret"); - - _clientSecretCredential = new ClientSecretCredentialBuilder() - .clientId(clientId) - .tenantId(tenantId) - .clientSecret(clientSecret) - .build(); - } - - if (_appClient == null) { - final TokenCredentialAuthProvider authProvider = - new TokenCredentialAuthProvider( - List.of("https://graph.microsoft.com/.default"), _clientSecretCredential); - - _appClient = GraphServiceClient.builder() - .authenticationProvider(authProvider) - .buildClient(); - } - } - // - - // - public static UserCollectionPage getUsers() throws Exception { - ensureGraphForAppOnlyAuth(); - - return _appClient.users() - .buildRequest() - .select("displayName,id,mail") - .top(25) - .orderBy("displayName") - .get(); - } - // - // public static void makeGraphCall() { // INSERT YOUR CODE HERE diff --git a/user-auth/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example b/user-auth/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example index 4b54e97..0b6ceb4 100644 --- a/user-auth/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example +++ b/user-auth/graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example @@ -1,5 +1,3 @@ app.clientId=YOUR_CLIENT_ID_HERE -app.clientSecret=YOUR_CLIENT_SECRET_HERE_IF_USING_APP_ONLY -app.tenantId=YOUR_TENANT_ID_HERE_IF_USING_APP_ONLY -app.authTenant=common +app.tenantId=common app.graphUserScopes=user.read,mail.read,mail.send diff --git a/user-auth/version b/user-auth/version index d3827e7..9459d4b 100644 --- a/user-auth/version +++ b/user-auth/version @@ -1 +1 @@ -1.0 +1.1 From 7417ff0434b05ec0bae81859dd14d51467a4444b Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:21:55 -0500 Subject: [PATCH 04/16] Update app-only sample --- .../main/java/graphapponlytutorial/App.java | 87 +--------- .../main/java/graphapponlytutorial/Graph.java | 152 +++--------------- .../oAuth.properties.example | 6 +- 3 files changed, 33 insertions(+), 212 deletions(-) diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java index a403457..b57ed56 100644 --- a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java @@ -5,23 +5,18 @@ package graphapponlytutorial; import java.io.IOException; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.InputMismatchException; import java.util.Properties; import java.util.Scanner; -import com.microsoft.graph.models.Message; import com.microsoft.graph.models.User; -import com.microsoft.graph.requests.MessageCollectionPage; import com.microsoft.graph.requests.UserCollectionPage; // public class App { // public static void main(String[] args) { - System.out.println("Java Graph Tutorial"); + System.out.println("Java App-Only Graph Tutorial"); System.out.println(); final Properties oAuthProperties = new Properties(); @@ -34,8 +29,6 @@ public static void main(String[] args) { initializeGraph(oAuthProperties); - greetUser(); - Scanner input = new Scanner(System.in); int choice = -1; @@ -44,10 +37,8 @@ public static void main(String[] args) { System.out.println("Please choose one of the following options:"); System.out.println("0. Exit"); System.out.println("1. Display access token"); - System.out.println("2. List my inbox"); - System.out.println("3. Send mail"); - System.out.println("4. List users (required app-only)"); - System.out.println("5. Make a Graph call"); + System.out.println("2. List users"); + System.out.println("3. Make a Graph call"); try { choice = input.nextInt(); @@ -68,18 +59,10 @@ public static void main(String[] args) { displayAccessToken(); break; case 2: - // List emails from user's inbox - listInbox(); - break; - case 3: - // Send an email message - sendMail(); - break; - case 4: // List users listUsers(); break; - case 5: + case 3: // Run any Graph code makeGraphCall(); break; @@ -95,7 +78,7 @@ public static void main(String[] args) { // private static void initializeGraph(Properties properties) { try { - Graph.initializeGraphForUserAuth(properties, + Graph.initializeGraphForAppOnlyAuth(properties, challenge -> System.out.println(challenge.getMessage())); } catch (Exception e) { @@ -105,26 +88,10 @@ private static void initializeGraph(Properties properties) { } // - // - private static void greetUser() { - try { - final User user = Graph.getUser(); - // For Work/school accounts, email is in mail property - // Personal accounts, email is in userPrincipalName - final String email = user.mail == null ? user.userPrincipalName : user.mail; - System.out.println("Hello, " + user.displayName + "!"); - System.out.println("Email: " + email); - } catch (Exception e) { - System.out.println("Error getting user"); - System.out.println(e.getMessage()); - } - } - // - // private static void displayAccessToken() { try { - final String accessToken = Graph.getUserToken(); + final String accessToken = Graph.getAppOnlyToken(); System.out.println("Access token: " + accessToken); } catch (Exception e) { System.out.println("Error getting access token"); @@ -133,48 +100,6 @@ private static void displayAccessToken() { } // - // - private static void listInbox() { - try { - final MessageCollectionPage messages = Graph.getInbox(); - - // Output each message's details - for (Message message: messages.getCurrentPage()) { - System.out.println("Message: " + message.subject); - System.out.println(" From: " + message.from.emailAddress.name); - System.out.println(" Status: " + (message.isRead ? "Read" : "Unread")); - System.out.println(" Received: " + message.receivedDateTime - // Values are returned in UTC, convert to local time zone - .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime() - .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT))); - } - - final Boolean moreMessagesAvailable = messages.getNextPage() != null; - System.out.println("\nMore messages available? " + moreMessagesAvailable); - } catch (Exception e) { - System.out.println("Error getting inbox"); - System.out.println(e.getMessage()); - } - } - // - - // - private static void sendMail() { - try { - // Send mail to the signed-in user - // Get the user for their email address - final User user = Graph.getUser(); - final String email = user.mail == null ? user.userPrincipalName : user.mail; - - Graph.sendMail("Testing Microsoft Graph", "Hello world!", email); - System.out.println("\nMail sent."); - } catch (Exception e) { - System.out.println("Error sending mail"); - System.out.println(e.getMessage()); - } - } - // - // private static void listUsers() { try { diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java index 8faeea9..01e8639 100644 --- a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java @@ -4,7 +4,6 @@ // package graphapponlytutorial; -import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.function.Consumer; @@ -13,31 +12,21 @@ import com.azure.core.credential.TokenRequestContext; import com.azure.identity.ClientSecretCredential; import com.azure.identity.ClientSecretCredentialBuilder; -import com.azure.identity.DeviceCodeCredential; -import com.azure.identity.DeviceCodeCredentialBuilder; import com.azure.identity.DeviceCodeInfo; import com.microsoft.graph.authentication.TokenCredentialAuthProvider; -import com.microsoft.graph.models.BodyType; -import com.microsoft.graph.models.EmailAddress; -import com.microsoft.graph.models.ItemBody; -import com.microsoft.graph.models.Message; -import com.microsoft.graph.models.Recipient; -import com.microsoft.graph.models.User; -import com.microsoft.graph.models.UserSendMailParameterSet; import com.microsoft.graph.requests.GraphServiceClient; -import com.microsoft.graph.requests.MessageCollectionPage; import com.microsoft.graph.requests.UserCollectionPage; import okhttp3.Request; // public class Graph { - // + // private static Properties _properties; - private static DeviceCodeCredential _deviceCodeCredential; - private static GraphServiceClient _userClient; + private static ClientSecretCredential _clientSecretCredential; + private static GraphServiceClient _appClient; - public static void initializeGraphForUserAuth(Properties properties, Consumer challenge) throws Exception { + public static void initializeGraphForAppOnlyAuth(Properties properties, Consumer challenge) throws Exception { // Ensure properties isn't null if (properties == null) { throw new Exception("Properties cannot be null"); @@ -45,114 +34,6 @@ public static void initializeGraphForUserAuth(Properties properties, Consumer graphUserScopes = Arrays - .asList(properties.getProperty("app.graphUserScopes").split(",")); - - _deviceCodeCredential = new DeviceCodeCredentialBuilder() - .clientId(clientId) - .tenantId(authTenantId) - .challengeConsumer(challenge) - .build(); - - final TokenCredentialAuthProvider authProvider = - new TokenCredentialAuthProvider(graphUserScopes, _deviceCodeCredential); - - _userClient = GraphServiceClient.builder() - .authenticationProvider(authProvider) - .buildClient(); - } - // - - // - public static String getUserToken() throws Exception { - // Ensure credential isn't null - if (_deviceCodeCredential == null) { - throw new Exception("Graph has not been initialized for user auth"); - } - - final String[] graphUserScopes = _properties.getProperty("app.graphUserScopes").split(","); - - final TokenRequestContext context = new TokenRequestContext(); - context.addScopes(graphUserScopes); - - final AccessToken token = _deviceCodeCredential.getToken(context).block(); - return token.getToken(); - } - // - - // - public static User getUser() throws Exception { - // Ensure client isn't null - if (_userClient == null) { - throw new Exception("Graph has not been initialized for user auth"); - } - - return _userClient.me() - .buildRequest() - .select("displayName,mail,userPrincipalName") - .get(); - } - // - - // - public static MessageCollectionPage getInbox() throws Exception { - // Ensure client isn't null - if (_userClient == null) { - throw new Exception("Graph has not been initialized for user auth"); - } - - return _userClient.me() - .mailFolders("inbox") - .messages() - .buildRequest() - .select("from,isRead,receivedDateTime,subject") - .top(25) - .orderBy("receivedDateTime DESC") - .get(); - } - // - - // - public static void sendMail(String subject, String body, String recipient) throws Exception { - // Ensure client isn't null - if (_userClient == null) { - throw new Exception("Graph has not been initialized for user auth"); - } - - // Create a new message - final Message message = new Message(); - message.subject = subject; - message.body = new ItemBody(); - message.body.content = body; - message.body.contentType = BodyType.TEXT; - - final Recipient toRecipient = new Recipient(); - toRecipient.emailAddress = new EmailAddress(); - toRecipient.emailAddress.address = recipient; - message.toRecipients = List.of(toRecipient); - - // Send the message - _userClient.me() - .sendMail(UserSendMailParameterSet.newBuilder() - .withMessage(message) - .build()) - .buildRequest() - .post(); - } - // - - // - private static ClientSecretCredential _clientSecretCredential; - private static GraphServiceClient _appClient; - - private static void ensureGraphForAppOnlyAuth() throws Exception { - // Ensure _properties isn't null - if (_properties == null) { - throw new Exception("Properties cannot be null"); - } - if (_clientSecretCredential == null) { final String clientId = _properties.getProperty("app.clientId"); final String tenantId = _properties.getProperty("app.tenantId"); @@ -177,9 +58,29 @@ private static void ensureGraphForAppOnlyAuth() throws Exception { } // + // + public static String getAppOnlyToken() throws Exception { + // Ensure credential isn't null + if (_clientSecretCredential == null) { + throw new Exception("Graph has not been initialized for app-only auth"); + } + + final String[] graphScopes = new String[] {"https://graph.microsoft.com/.default"}; + + final TokenRequestContext context = new TokenRequestContext(); + context.addScopes(graphScopes); + + final AccessToken token = _clientSecretCredential.getToken(context).block(); + return token.getToken(); + } + // + // public static UserCollectionPage getUsers() throws Exception { - ensureGraphForAppOnlyAuth(); + // Ensure client isn't null + if (_appClient == null) { + throw new Exception("Graph has not been initialized for app-only auth"); + } return _appClient.users() .buildRequest() @@ -193,9 +94,6 @@ public static UserCollectionPage getUsers() throws Exception { // public static void makeGraphCall() { // INSERT YOUR CODE HERE - // Note: if using _appClient, be sure to call ensureGraphForAppOnlyAuth - // before using it. - // ensureGraphForAppOnlyAuth(); } // } diff --git a/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example b/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example index 4b54e97..baa4f9e 100644 --- a/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example +++ b/app-auth/graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example @@ -1,5 +1,3 @@ app.clientId=YOUR_CLIENT_ID_HERE -app.clientSecret=YOUR_CLIENT_SECRET_HERE_IF_USING_APP_ONLY -app.tenantId=YOUR_TENANT_ID_HERE_IF_USING_APP_ONLY -app.authTenant=common -app.graphUserScopes=user.read,mail.read,mail.send +app.clientSecret=YOUR_CLIENT_SECRET_HERE +app.tenantId=YOUR_TENANT_ID_HERE From e08a8a069200178d44dbddc7ee33570e628c3f18 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:33:33 -0500 Subject: [PATCH 05/16] Update READMEs --- app-auth/README.md | 71 ++++++++++----------------------------------- user-auth/README.md | 49 ++----------------------------- 2 files changed, 17 insertions(+), 103 deletions(-) diff --git a/app-auth/README.md b/app-auth/README.md index bd9d8a9..58efa9e 100644 --- a/app-auth/README.md +++ b/app-auth/README.md @@ -5,7 +5,7 @@ To run the completed project in this folder, you need the following: - [Java SE Development Kit (JDK)](https://java.com/en/download/faq/develop.xml) and [Gradle](https://gradle.org/) installed on your development machine. (**Note:** This tutorial was written with OpenJDK version 17.0.2 and Gradle 7.4.2. The steps in this guide may work with other versions, but that has not been tested.) -- A Microsoft work or school account. +- A Microsoft work or school account with the **Global administrator** role. If you don't have a Microsoft account, you can [sign up for the Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) to get a free Microsoft 365 subscription. @@ -13,33 +13,19 @@ If you don't have a Microsoft account, you can [sign up for the Microsoft 365 De You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/get-started). -**NOTE:** If you downloaded this code from [https://developer.microsoft.com/graph/quick-start](https://developer.microsoft.com/graph/quick-start), an app registration has already been created for you. However, if you want to use the app-only portion of this sample, you will need to modify the app registration as specified in [Configure app-only auth (AAD admin center)](#configure-app-only-auth-aad-admin-center) or [Configure app-only auth (PowerShell)](#configure-app-only-auth-powershell). - ### Azure Active Directory admin center -1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com) and login using a **personal account** (aka: Microsoft Account) or **Work or School Account**. +1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com) and login using a Global administrator account. 1. Select **Azure Active Directory** in the left-hand navigation, then select **App registrations** under **Manage**. -1. Select **New registration**. Enter a name for your application, for example, `Java Graph Tutorial`. - -1. Set **Supported account types** as desired. The options are: +1. Select **New registration**. Enter a name for your application, for example, `Java App-Only Graph Tutorial`. - | Option | Who can sign in? | - |--------|------------------| - | **Accounts in this organizational directory only** | Only users in your Microsoft 365 organization | - | **Accounts in any organizational directory** | Users in any Microsoft 365 organization (work or school accounts) | - | **Accounts in any organizational directory ... and personal Microsoft accounts** | Users in any Microsoft 365 organization (work or school accounts) and personal Microsoft accounts | +1. Set **Supported account types** to **Accounts in this organizational directory only**. 1. Leave **Redirect URI** empty. -1. Select **Register**. On the application's **Overview** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step. If you chose **Accounts in this organizational directory only** for **Supported account types**, also copy the **Directory (tenant) ID** and save it. - -1. Select **Authentication** under **Manage**. Locate the **Advanced settings** section and change the **Allow public client flows** toggle to **Yes**, then choose **Save**. - -#### Configure app-only auth (AAD admin center) - -> **Note:** This section requires a work/school account with the Global administrator role. You only need to complete these steps if you plan on using the app-only portions of this sample. +1. Select **Register**. On the application's **Overview** page, copy the value of the **Application (client) ID** and **Directory (tenant) ID** and save them, you will need these values in the next step. 1. Select **API permissions** under **Manage**. @@ -63,58 +49,31 @@ You can register an application using the Azure Active Directory admin center, o To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/installation) for installation instructions. -1. Open PowerShell and run the [RegisterAppForUserAuth.ps1](RegisterAppForUserAuth.ps1) file with the following command, replacing *<audience-value>* with the desired value (see table below). - - > **Note:** The RegisterAppForUserAuth.ps1 script requires a work/school account with the Application administrator, Cloud application administrator, or Global administrator role. - - ```powershell - .\RegisterAppForUserAuth.ps1 -AppName "Java Graph Tutorial" -SignInAudience - ``` - - | SignInAudience value | Who can sign in? | - |----------------------|------------------| - | `AzureADMyOrg` | Only users in your Microsoft 365 organization | - | `AzureADMultipleOrgs` | Users in any Microsoft 365 organization (work or school accounts) | - | `AzureADandPersonalMicrosoftAccount` | Users in any Microsoft 365 organization (work or school accounts) and personal Microsoft accounts | - | `PersonalMicrosoftAccount` | Only personal Microsoft accounts | - -1. Copy the **Client ID** and **Auth tenant** values from the script output. You will need these values in the next step. - - ```powershell - SUCCESS - Client ID: 2fb1652f-a9a0-4db9-b220-b224b8d9d38b - Auth tenant: common - ``` - -#### Configure app-only auth (PowerShell) - -> **Note:** This section requires a work/school account with the Global administrator role. You only need to complete these steps if you plan on using the app-only portions of this sample. - -1. Run the [UpdateAppForAppOnlyAuth.ps1](UpdateAppForAppOnlyAuth.ps1) file with the following command, replacing *<your-client-id>* with your client ID. +1. Open PowerShell and run the [RegisterAppForAppOnlyAuth.ps1](RegisterAppForAppOnlyAuth.ps1) file with the following command. ```powershell - .\UpdateAppForAppOnlyAuth.ps1 -AppId -GraphScopes "User.Read.All" + .\RegisterAppForAppOnlyAuth.ps1 -AppName "Java App-Only Graph Tutorial" -GraphScopes "User.Read.All" ``` -1. Copy the **Tenant ID** and **Client secret** values from the script output. You will need these values in the next step. +1. Copy the **Client ID**, **Tenant ID**, and **Client secret** values from the script output. You will need these values in the next step. ```powershell SUCCESS - Tenant ID: a795ad0f-7d82-4a3b-a2c0-0713ec72ade7 - Client secret: 2jv7Q~8eiOd_QafJ..... - Secret expires: 2/16/2024 9:32:09 PM + Client ID: ae2386e6-799e-4f75-b191-855d7e691c75 + Tenant ID: 5927c10a-91bd-4408-9c70-c50bce922b71 + Client secret: ... + Secret expires: 10/28/2024 5:01:45 PM ``` ## Configure the sample -1. Rename [oAuth.properties.example](./graphtutorial/app/src/main/resources/oAuth.properties.example) to `oauthProperties` and update the values according to the following table. +1. Rename [oAuth.properties.example](./graphapponlytutorial/app/src/main/resources/graphapponlytutorial/oAuth.properties.example) to `oauthProperties` and update the values according to the following table. | Setting | Value | |---------|-------| | `app.clientId` | The client ID of your app registration | - | `app.clientSecret` | The client secret (only needed if doing app-only) | - | `app.tenantId` | The tenant ID of your organization (only needed if doing app-only) | - | `app.authTenant` | If you chose the option to only allow users in your organization to sign in, change this value to your tenant ID. Otherwise leave as `common`. | + | `app.clientSecret` | The client secret | + | `app.tenantId` | The tenant ID of your organization | ## Build and run the sample diff --git a/user-auth/README.md b/user-auth/README.md index bd9d8a9..5aaf67e 100644 --- a/user-auth/README.md +++ b/user-auth/README.md @@ -13,8 +13,6 @@ If you don't have a Microsoft account, you can [sign up for the Microsoft 365 De You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/get-started). -**NOTE:** If you downloaded this code from [https://developer.microsoft.com/graph/quick-start](https://developer.microsoft.com/graph/quick-start), an app registration has already been created for you. However, if you want to use the app-only portion of this sample, you will need to modify the app registration as specified in [Configure app-only auth (AAD admin center)](#configure-app-only-auth-aad-admin-center) or [Configure app-only auth (PowerShell)](#configure-app-only-auth-powershell). - ### Azure Active Directory admin center 1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com) and login using a **personal account** (aka: Microsoft Account) or **Work or School Account**. @@ -37,28 +35,6 @@ You can register an application using the Azure Active Directory admin center, o 1. Select **Authentication** under **Manage**. Locate the **Advanced settings** section and change the **Allow public client flows** toggle to **Yes**, then choose **Save**. -#### Configure app-only auth (AAD admin center) - -> **Note:** This section requires a work/school account with the Global administrator role. You only need to complete these steps if you plan on using the app-only portions of this sample. - -1. Select **API permissions** under **Manage**. - -1. Remove the default **User.Read** permission under **Configured permissions** by selecting the ellipses (**...**) in its row and selecting **Remove permission**. - -1. Select **Add a permission**, then **Microsoft Graph**. - -1. Select **Application permissions**. - -1. Select **User.Read.All**, then select **Add permissions**. - -1. Select **Grant admin consent for...**, then select **Yes** to provide admin consent for the selected permission. - -1. Select **Certificates and secrets** under **Manage**, then select **New client secret**. - -1. Enter a description, choose a duration, and select **Add**. - -1. Copy the secret from the **Value** column, you will need it in the next steps. - ### PowerShell To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/installation) for installation instructions. @@ -86,35 +62,14 @@ To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not Auth tenant: common ``` -#### Configure app-only auth (PowerShell) - -> **Note:** This section requires a work/school account with the Global administrator role. You only need to complete these steps if you plan on using the app-only portions of this sample. - -1. Run the [UpdateAppForAppOnlyAuth.ps1](UpdateAppForAppOnlyAuth.ps1) file with the following command, replacing *<your-client-id>* with your client ID. - - ```powershell - .\UpdateAppForAppOnlyAuth.ps1 -AppId -GraphScopes "User.Read.All" - ``` - -1. Copy the **Tenant ID** and **Client secret** values from the script output. You will need these values in the next step. - - ```powershell - SUCCESS - Tenant ID: a795ad0f-7d82-4a3b-a2c0-0713ec72ade7 - Client secret: 2jv7Q~8eiOd_QafJ..... - Secret expires: 2/16/2024 9:32:09 PM - ``` - ## Configure the sample -1. Rename [oAuth.properties.example](./graphtutorial/app/src/main/resources/oAuth.properties.example) to `oauthProperties` and update the values according to the following table. +1. Rename [oAuth.properties.example](./graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example) to `oauthProperties` and update the values according to the following table. | Setting | Value | |---------|-------| | `app.clientId` | The client ID of your app registration | - | `app.clientSecret` | The client secret (only needed if doing app-only) | - | `app.tenantId` | The tenant ID of your organization (only needed if doing app-only) | - | `app.authTenant` | If you chose the option to only allow users in your organization to sign in, change this value to your tenant ID. Otherwise leave as `common`. | + | `app.tenantId` | If you chose the option to only allow users in your organization to sign in, change this value to your tenant ID. Otherwise leave as `common`. | ## Build and run the sample From 1a248a39e43f5af57d357ad07aaaa7c40ae33591 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:39:03 -0500 Subject: [PATCH 06/16] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ba6debb..68616e3 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# Microsoft Graph Training Module - Build Java apps with the Microsoft Graph Java SDK +# Build Java apps with the Microsoft Graph Java SDK - Completed project -This module will introduce you to working with the Microsoft Graph SDK to access data in Office 365 by building Java applications. +[![Java CI with Gradle](https://github.com/microsoftgraph/msgraph-training-java/actions/workflows/gradle.yml/badge.svg)](https://github.com/microsoftgraph/msgraph-training-java/actions/workflows/gradle.yml) ![License.](https://img.shields.io/badge/license-MIT-green.svg) -## Lab - Build Java apps with the Microsoft Graph Java SDK +This sample will introduce you to working with the Microsoft Graph SDK to access data in Microsoft 365 from Java applications. This code is the result of completing the [Java Microsoft Graph tutorial](https://docs.microsoft.com/graph/tutorials/java) and the [Java Microsoft Graph app-only tutorial](https://docs.microsoft.com/graph/tutorials/java-app-only). -In this lab you will create a console application using the Microsoft Authentication Library (MSAL) to access data in Office 365 using the Microsoft Graph. +## Running the sample -- [Java Microsoft Graph tutorial](https://docs.microsoft.com/graph/tutorials/java) +The code for the delegated user authentication sample is in the [user-auth](user-auth) folder. Instructions to configure and run the sample can be found in the [README](user-auth/README.md) in that folder. -## Completed sample +The code for the app-only authentication sample is in the [app-auth](app-auth) folder. Instructions to configure and run the sample can be found in the [README](app-auth/README.md) in that folder. -If you just want the completed sample generated by following this lab, you can find it here. +## Code of conduct -- [Completed project](demo) +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Disclaimer From 8c2a4c20d0007b847711cb598f61d4137b247c39 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:42:11 -0500 Subject: [PATCH 07/16] Update qs.json --- qs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qs.json b/qs.json index d8d7366..e07175c 100644 --- a/qs.json +++ b/qs.json @@ -1,5 +1,5 @@ { - "sourceDirectory": "./demo", + "sourceDirectory": "./user-auth", "exampleConfigFile": "./graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example", "configFile": "oAuth.properties", "archiveFile": "JavaQuickStart.zip", From 095a3f5fbccebc4dd13cbd57bd444c9d645617b7 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:43:56 -0500 Subject: [PATCH 08/16] Update dependabot.yml --- .github/dependabot.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 51b2754..d53aba7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,10 @@ version: 2 updates: - package-ecosystem: "gradle" # See documentation for possible values - directory: "/demo/graphtutorial/" # Location of package manifests + directory: "/user-auth/graphtutorial/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "gradle" # See documentation for possible values + directory: "/app-auth/graphapponlytutorial/" # Location of package manifests schedule: interval: "weekly" From ed4aaa7978d18b984bc437db61c8d5340e7234a3 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:46:35 -0500 Subject: [PATCH 09/16] Update gradle.yml --- .github/workflows/gradle.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index bd2f635..3eeab97 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -18,9 +18,6 @@ permissions: jobs: build: - defaults: - run: - working-directory: demo/graphtutorial runs-on: ubuntu-latest @@ -31,10 +28,17 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Make gradlew executable - run: chmod +x ./gradlew - - name: Build with Gradle + - name: Make user app gradlew executable + run: chmod +x ./user-auth/graphtutorial/gradlew + - name: Build user app with Gradle uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee with: - build-root-directory: demo/graphtutorial + build-root-directory: user-app/graphtutorial + arguments: build + - name: Make app-only app gradlew executable + run: chmod +x ./app-auth/graphapponlytutorial/gradlew + - name: Build app-only app with Gradle + uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee + with: + build-root-directory: app-auth/graphapponlytutorial arguments: build From 2a0d28d94cc2be377dfd300ded7bb74b90ec97dd Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:53:50 -0500 Subject: [PATCH 10/16] Update docs domain in links --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- README.md | 2 +- app-auth/README.md | 4 ++-- user-auth/README.md | 4 ++-- user-auth/RegisterAppForUserAuth.ps1 | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 83e3a9c..e43fcfe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,7 +10,7 @@ assignees: '' - [ ] Downloaded from GitHub - [ ] Downloaded from the [Microsoft Graph quick start tool](https://developer.microsoft.com/graph/quick-start) -- [ ] Followed the tutorial from [Microsoft Graph tutorials](https://docs.microsoft.com/graph/tutorials) +- [ ] Followed the tutorial from [Microsoft Graph tutorials](https://learn.microsoft.com/graph/tutorials) ### Describe the bug diff --git a/README.md b/README.md index 68616e3..d7263db 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Java CI with Gradle](https://github.com/microsoftgraph/msgraph-training-java/actions/workflows/gradle.yml/badge.svg)](https://github.com/microsoftgraph/msgraph-training-java/actions/workflows/gradle.yml) ![License.](https://img.shields.io/badge/license-MIT-green.svg) -This sample will introduce you to working with the Microsoft Graph SDK to access data in Microsoft 365 from Java applications. This code is the result of completing the [Java Microsoft Graph tutorial](https://docs.microsoft.com/graph/tutorials/java) and the [Java Microsoft Graph app-only tutorial](https://docs.microsoft.com/graph/tutorials/java-app-only). +This sample will introduce you to working with the Microsoft Graph SDK to access data in Microsoft 365 from Java applications. This code is the result of completing the [Java Microsoft Graph tutorial](https://learn.microsoft.com/graph/tutorials/java) and the [Java Microsoft Graph app-only tutorial](https://learn.microsoft.com/graph/tutorials/java-app-only). ## Running the sample diff --git a/app-auth/README.md b/app-auth/README.md index 58efa9e..aef2fe0 100644 --- a/app-auth/README.md +++ b/app-auth/README.md @@ -11,7 +11,7 @@ If you don't have a Microsoft account, you can [sign up for the Microsoft 365 De ## Register an application -You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/get-started). +You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://learn.microsoft.com/graph/powershell/get-started). ### Azure Active Directory admin center @@ -47,7 +47,7 @@ You can register an application using the Azure Active Directory admin center, o ### PowerShell -To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/installation) for installation instructions. +To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://learn.microsoft.com/graph/powershell/installation) for installation instructions. 1. Open PowerShell and run the [RegisterAppForAppOnlyAuth.ps1](RegisterAppForAppOnlyAuth.ps1) file with the following command. diff --git a/user-auth/README.md b/user-auth/README.md index 5aaf67e..97d10f7 100644 --- a/user-auth/README.md +++ b/user-auth/README.md @@ -11,7 +11,7 @@ If you don't have a Microsoft account, you can [sign up for the Microsoft 365 De ## Register an application -You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/get-started). +You can register an application using the Azure Active Directory admin center, or by using the [Microsoft Graph PowerShell SDK](https://learn.microsoft.com/graph/powershell/get-started). ### Azure Active Directory admin center @@ -37,7 +37,7 @@ You can register an application using the Azure Active Directory admin center, o ### PowerShell -To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://docs.microsoft.com/graph/powershell/installation) for installation instructions. +To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not have it, see [Install the Microsoft Graph PowerShell SDK](https://learn.microsoft.com/graph/powershell/installation) for installation instructions. 1. Open PowerShell and run the [RegisterAppForUserAuth.ps1](RegisterAppForUserAuth.ps1) file with the following command, replacing *<audience-value>* with the desired value (see table below). diff --git a/user-auth/RegisterAppForUserAuth.ps1 b/user-auth/RegisterAppForUserAuth.ps1 index 45fcb35..0b8598a 100644 --- a/user-auth/RegisterAppForUserAuth.ps1 +++ b/user-auth/RegisterAppForUserAuth.ps1 @@ -21,7 +21,7 @@ param( ) # Tenant to use in authentication. -# See https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-request +# See https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-request $authTenant = switch ($SignInAudience) { "AzureADMyOrg" { "tenantId" } From cdd760e777ded95166296f8d112089cfce01148a Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 15:53:54 -0500 Subject: [PATCH 11/16] Update CONTRIBUTING.md --- CONTRIBUTING.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17305e4..9e87646 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,15 +4,12 @@ Thank you for contributing to this project! Before submitting your pull request, ## Overview -The code in this repository serves three purposes: +The code in this repository serves two purposes: -- The Markdown files in the [tutorial](/tutorial) folder are published as a tutorial on the [Microsoft Graph tutorials](https://docs.microsoft.com/graph/tutorials) page. -- The sample project in the [demo](/demo) folder is the source for a [Microsoft Graph quick start](https://developer.microsoft.com/graph/quick-start).**\*** -- The sample project in the demo folder is also downloadable directly from GitHub and should run as-is after some simple configuration. +- The sample project in the [user-auth](/user-auth) folder is the source for a [Microsoft Graph quick start](https://developer.microsoft.com/graph/quick-start). +- The code in the [user-auth](/user-auth) and [app-auth](/app-auth) folders are directly referenced by [Microsoft Graph tutorials](https://learn.microsoft.com/graph/tutorials) -> **\*** Not all training repositories are available as quick starts (yet). - -This is important to keep in mind, because changes in one place *may* require changes in another, to keep things in sync. Whereever possible, the Markdown files refer to the source code files directly (using a custom `:::code` syntax), so that updating code in source will automatically update the code in Markdown. +This is important to keep in mind, because changes in one place *may* require changes in another, to keep things in sync. The Markdown files for the tutorials refer to the source code files directly (using a custom `:::code` syntax), so that updating code in source will automatically update the code in Markdown. ## Updating code @@ -24,7 +21,7 @@ Console.WriteLine("Hello World!"); // ``` -If you update code between these "marker" comments, the Markdown files will automatically get those changes when published to the Microsoft Graph documentation site. If you update code outside of those comments, it's very likely that you'll need to update the corresponding Markdown. +If you update code between these "marker" comments, the Markdown files will automatically get those changes when published to the Microsoft Graph documentation site. If you update code outside of those comments, it's very likely that you'll need to update the corresponding Markdown in the [Microsoft Graph training repository](https://github.com/microsoftgraph/microsoft-graph-training). ## Adding features @@ -32,11 +29,11 @@ While the enthusiasm is appreciated, please don't send pull requests to add new ## Submitting pull requests -Please submit all pull requests to the `master` branch. +Please submit all pull requests to the `main` branch. ## When do changes get published? -Publishing of updates to the [Microsoft Graph tutorials](https://docs.microsoft.com/graph/tutorials) site is not automatic. Changes must first be promoted to the `live` branch, then a build must be triggered by the site admins. This is typically done on an "as-needed" basis. +Publishing of updates to the [Microsoft Graph tutorials](https://learn.microsoft.com/graph/tutorials) site is not automatic. Changes must first be promoted to the `live` branch, then a build must be triggered by the site admins. This is typically done on an "as-needed" basis. ## Code of conduct From bee366bb561050acc8cc927931938052429dd34a Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 10 Nov 2022 16:48:19 -0500 Subject: [PATCH 12/16] Cleanup based on review --- .../app/src/main/java/graphapponlytutorial/App.java | 3 +-- .../app/src/main/java/graphapponlytutorial/Graph.java | 4 +--- .../graphtutorial/app/src/main/java/graphtutorial/Graph.java | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java index b57ed56..c4ecf23 100644 --- a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/App.java @@ -78,8 +78,7 @@ public static void main(String[] args) { // private static void initializeGraph(Properties properties) { try { - Graph.initializeGraphForAppOnlyAuth(properties, - challenge -> System.out.println(challenge.getMessage())); + Graph.initializeGraphForAppOnlyAuth(properties); } catch (Exception e) { System.out.println("Error initializing Graph for user auth"); diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java index 01e8639..16bb8b5 100644 --- a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java @@ -6,13 +6,11 @@ import java.util.List; import java.util.Properties; -import java.util.function.Consumer; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenRequestContext; import com.azure.identity.ClientSecretCredential; import com.azure.identity.ClientSecretCredentialBuilder; -import com.azure.identity.DeviceCodeInfo; import com.microsoft.graph.authentication.TokenCredentialAuthProvider; import com.microsoft.graph.requests.GraphServiceClient; import com.microsoft.graph.requests.UserCollectionPage; @@ -26,7 +24,7 @@ public class Graph { private static ClientSecretCredential _clientSecretCredential; private static GraphServiceClient _appClient; - public static void initializeGraphForAppOnlyAuth(Properties properties, Consumer challenge) throws Exception { + public static void initializeGraphForAppOnlyAuth(Properties properties) throws Exception { // Ensure properties isn't null if (properties == null) { throw new Exception("Properties cannot be null"); diff --git a/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java b/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java index 464eb9f..051166c 100644 --- a/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java +++ b/user-auth/graphtutorial/app/src/main/java/graphtutorial/Graph.java @@ -143,9 +143,6 @@ public static void sendMail(String subject, String body, String recipient) throw // public static void makeGraphCall() { // INSERT YOUR CODE HERE - // Note: if using _appClient, be sure to call ensureGraphForAppOnlyAuth - // before using it. - // ensureGraphForAppOnlyAuth(); } // } From 2ddc77c4af9b6b224d8036171c4c4749be5eb70e Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Fri, 11 Nov 2022 16:14:10 -0500 Subject: [PATCH 13/16] Update Graph.java --- .../app/src/main/java/graphapponlytutorial/Graph.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java index 16bb8b5..0518521 100644 --- a/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java +++ b/app-auth/graphapponlytutorial/app/src/main/java/graphapponlytutorial/Graph.java @@ -47,6 +47,7 @@ public static void initializeGraphForAppOnlyAuth(Properties properties) throws E if (_appClient == null) { final TokenCredentialAuthProvider authProvider = new TokenCredentialAuthProvider( + // Use the .default scope when using app-only auth List.of("https://graph.microsoft.com/.default"), _clientSecretCredential); _appClient = GraphServiceClient.builder() @@ -63,6 +64,7 @@ public static String getAppOnlyToken() throws Exception { throw new Exception("Graph has not been initialized for app-only auth"); } + // Request the .default scope as required by app-only auth final String[] graphScopes = new String[] {"https://graph.microsoft.com/.default"}; final TokenRequestContext context = new TokenRequestContext(); From e637170d6382e8a45b1b0cd85556bbb863b39e00 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Wed, 7 Dec 2022 11:56:36 -0500 Subject: [PATCH 14/16] Update README.md --- user-auth/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user-auth/README.md b/user-auth/README.md index 97d10f7..fb98daa 100644 --- a/user-auth/README.md +++ b/user-auth/README.md @@ -5,9 +5,9 @@ To run the completed project in this folder, you need the following: - [Java SE Development Kit (JDK)](https://java.com/en/download/faq/develop.xml) and [Gradle](https://gradle.org/) installed on your development machine. (**Note:** This tutorial was written with OpenJDK version 17.0.2 and Gradle 7.4.2. The steps in this guide may work with other versions, but that has not been tested.) -- A Microsoft work or school account. - -If you don't have a Microsoft account, you can [sign up for the Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) to get a free Microsoft 365 subscription. +- Either a personal Microsoft account with a mailbox on Outlook.com, or a Microsoft work or school account. If you don't have a Microsoft account, there are a couple of options to get a free account: + - You can [sign up for a new personal Microsoft account](https://signup.live.com/signup?wa=wsignin1.0&rpsnv=12&ct=1454618383&rver=6.4.6456.0&wp=MBI_SSL_SHARED&wreply=https://mail.live.com/default.aspx&id=64855&cbcxt=mai&bk=1454618383&uiflavor=web&uaid=b213a65b4fdc484382b6622b3ecaa547&mkt=E-US&lc=1033&lic=1). + - You can [sign up for the Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) to get a free Microsoft 365 subscription. ## Register an application @@ -71,7 +71,7 @@ To use PowerShell, you'll need the Microsoft Graph PowerShell SDK. If you do not | `app.clientId` | The client ID of your app registration | | `app.tenantId` | If you chose the option to only allow users in your organization to sign in, change this value to your tenant ID. Otherwise leave as `common`. | -## Build and run the sample +## Run the sample In your command-line interface (CLI), navigate to the project directory and run the following command. From f7ad1d64fc54998cf0e74e92e6149479e08e095b Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Wed, 7 Dec 2022 12:01:11 -0500 Subject: [PATCH 15/16] Update gradle.yml --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 3eeab97..a8d019f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -33,7 +33,7 @@ jobs: - name: Build user app with Gradle uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee with: - build-root-directory: user-app/graphtutorial + build-root-directory: user-auth/graphtutorial arguments: build - name: Make app-only app gradlew executable run: chmod +x ./app-auth/graphapponlytutorial/gradlew From 902946333e05548a1d6f911e876f29a1a4a091f9 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Wed, 7 Dec 2022 12:40:47 -0500 Subject: [PATCH 16/16] Update qs.json --- qs.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qs.json b/qs.json index e07175c..572c5f8 100644 --- a/qs.json +++ b/qs.json @@ -3,5 +3,8 @@ "exampleConfigFile": "./graphtutorial/app/src/main/resources/graphtutorial/oAuth.properties.example", "configFile": "oAuth.properties", "archiveFile": "JavaQuickStart.zip", - "zipReadMe": "./README.md" + "zipReadMe": "./README.md", + "excludeFiles": [ + "user-auth/RegisterAppForUserAuth.ps1" + ] }