Build Go apps with Microsoft Graph
This tutorial teaches you how to build a Go console app that uses the Microsoft Graph API to access data on behalf of a user.
Note
To learn how to use Microsoft Graph to access data using app-only authentication, see this app-only authentication tutorial.
In this tutorial, you will:
Tip
As an alternative to following this tutorial, you can download the completed code through the quick start tool, which automates app registration and configuration. The downloaded code works without any modifications required.
You can also download or clone the GitHub repository and follow the instructions in the README to register an application and configure the project.
Prerequisites
Before you start this tutorial, you should have Go installed on your development machine.
You should also have a Microsoft work or school account with an Exchange Online mailbox. If you don't have a Microsoft 365 tenant, you might qualify for one through the Microsoft 365 Developer Program; for details, see the FAQ. Alternatively, you can sign up for a 1-month free trial or purchase a Microsoft 365 plan.
Note
This tutorial was written with Go version 1.19.3. The steps in this guide may work with other versions, but that has not been tested.
Register the app in the portal
In this exercise you will register a new application in Azure Active Directory to enable user authentication. You can register an application using the Microsoft Entra admin center, or by using the Microsoft Graph PowerShell SDK.
Register application for user authentication
In this section you will register an application that supports user authentication using device code flow.
Open a browser and navigate to the Microsoft Entra admin center and login using a Global administrator account.
Select Microsoft Entra ID in the left-hand navigation, expand Identity, expand Applications, then select App registrations.
Select New registration. Enter a name for your application, for example,
Graph User Auth Tutorial
.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 Leave Redirect URI empty.
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.
Select Authentication under Manage. Locate the Advanced settings section and change the Allow public client flows toggle to Yes, then choose Save.
Note
Notice that you did not configure any Microsoft Graph permissions on the app registration. This is because the sample uses dynamic consent to request specific permissions for user authentication.
Create a Go console app
Begin by initializing a new Go module using the Go CLI. Open your command-line interface (CLI) in a directory where you want to create the project. Run the following command.
go mod init graphtutorial
Install dependencies
Before moving on, add some additional dependencies that you will use later.
- Azure Identity Client Module for Go to authenticate the user and acquire access tokens.
- Microsoft Graph SDK for Go to make calls to the Microsoft Graph.
- GoDotEnv for reading environment variables from .env files.
Run the following commands in your CLI to install the dependencies.
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
go get github.com/microsoftgraph/msgraph-sdk-go
go get github.com/joho/godotenv
Load application settings
In this section you'll add the details of your app registration to the project.
Create a file in the same directory as go.mod named .env and add the following code.
CLIENT_ID=YOUR_CLIENT_ID_HERE TENANT_ID=common GRAPH_USER_SCOPES=user.read,mail.read,mail.send
Update the values according to the following table.
Setting Value CLIENT_ID
The client ID of your app registration TENANT_ID
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
.Tip
Optionally, you can set these values in a separate file named .env.local.
Design the app
In this section you will create a simple console-based menu.
Create a new directory in the same directory as go.mod named graphhelper.
Add a new file in the graphhelper directory named graphhelper.go and add the following code.
package graphhelper import ( "context" "fmt" "os" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" auth "github.com/microsoft/kiota-authentication-azure-go" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/users" ) type GraphHelper struct { deviceCodeCredential *azidentity.DeviceCodeCredential userClient *msgraphsdk.GraphServiceClient graphUserScopes []string } func NewGraphHelper() *GraphHelper { g := &GraphHelper{} return g }
This creates a basic GraphHelper type that you will extend in later sections to use Microsoft Graph.
Create a file in the same directory as go.mod named graphtutorial.go. Add the following code.
package main import ( "fmt" "graphtutorial/graphhelper" "log" "time" "github.com/joho/godotenv" ) func main() { fmt.Println("Go Graph Tutorial") fmt.Println() // Load .env files // .env.local takes precedence (if present) godotenv.Load(".env.local") err := godotenv.Load() if err != nil { log.Fatal("Error loading .env") } graphHelper := graphhelper.NewGraphHelper() initializeGraph(graphHelper) greetUser(graphHelper) var choice int64 = -1 for { fmt.Println("Please choose one of the following options:") fmt.Println("0. Exit") fmt.Println("1. Display access token") fmt.Println("2. List my inbox") fmt.Println("3. Send mail") fmt.Println("4. Make a Graph call") _, err = fmt.Scanf("%d", &choice) if err != nil { choice = -1 } switch choice { case 0: // Exit the program fmt.Println("Goodbye...") case 1: // Display access token displayAccessToken(graphHelper) case 2: // List emails from user's inbox listInbox(graphHelper) case 3: // Send an email message sendMail(graphHelper) case 4: // Run any Graph code makeGraphCall(graphHelper) default: fmt.Println("Invalid choice! Please try again.") } if choice == 0 { break } } }
Add the following placeholder methods at the end of the file. You'll implement them in later steps.
func initializeGraph(graphHelper *graphhelper.GraphHelper) { // TODO } func greetUser(graphHelper *graphhelper.GraphHelper) { // TODO } func displayAccessToken(graphHelper *graphhelper.GraphHelper) { // TODO } func listInbox(graphHelper *graphhelper.GraphHelper) { // TODO } func sendMail(graphHelper *graphhelper.GraphHelper) { // TODO } func makeGraphCall(graphHelper *graphhelper.GraphHelper) { // TODO }
This implements a basic menu and reads the user's choice from the command line.
Add user authentication
In this section you will extend the application from the previous exercise to support authentication with Azure AD. This is required to obtain the necessary OAuth access token to call the Microsoft Graph. In this step you will integrate the Azure Identity Client Module for Go into the application and configure authentication for the Microsoft Graph SDK for Go.
The Azure Identity library provides a number of TokenCredential
classes that implement OAuth2 token flows. The Microsoft Graph client library uses those classes to authenticate calls to Microsoft Graph.
Configure Graph client for user authentication
In this section you will use the DeviceCodeCredential
class to request an access token by using the device code flow.
Add the following function to ./graphhelper/graphhelper.go.
func (g *GraphHelper) InitializeGraphForUserAuth() error { clientId := os.Getenv("CLIENT_ID") tenantId := os.Getenv("TENANT_ID") scopes := os.Getenv("GRAPH_USER_SCOPES") g.graphUserScopes = strings.Split(scopes, ",") // Create the device code credential credential, err := azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{ ClientID: clientId, TenantID: tenantId, UserPrompt: func(ctx context.Context, message azidentity.DeviceCodeMessage) error { fmt.Println(message.Message) return nil }, }) if err != nil { return err } g.deviceCodeCredential = credential // Create an auth provider using the credential authProvider, err := auth.NewAzureIdentityAuthenticationProviderWithScopes(credential, g.graphUserScopes) if err != nil { return err } // Create a request adapter using the auth provider adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider) if err != nil { return err } // Create a Graph client using request adapter client := msgraphsdk.NewGraphServiceClient(adapter) g.userClient = client return nil }
Tip
If you are using goimports, some modules may have been auto-removed from your
import
statement in graphhelper.go on save. You may need to re-add the modules to build.Replace the empty
initializeGraph
function in graphtutorial.go with the following.func initializeGraph(graphHelper *graphhelper.GraphHelper) { err := graphHelper.InitializeGraphForUserAuth() if err != nil { log.Panicf("Error initializing Graph for user auth: %v\n", err) } }
This code initializes two properties, a DeviceCodeCredential
object and a GraphServiceClient
object. The InitializeGraphForUserAuth
function creates a new instance of DeviceCodeCredential
, then uses that instance to create a new instance of GraphServiceClient
. Every time an API call is made to Microsoft Graph through the userClient
, it will use the provided credential to get an access token.
Test the DeviceCodeCredential
Next, add code to get an access token from the DeviceCodeCredential
.
Add the following function to ./graphhelper/graphhelper.go.
func (g *GraphHelper) GetUserToken() (*string, error) { token, err := g.deviceCodeCredential.GetToken(context.Background(), policy.TokenRequestOptions{ Scopes: g.graphUserScopes, }) if err != nil { return nil, err } return &token.Token, nil }
Replace the empty
displayAccessToken
function in graphtutorial.go with the following.func displayAccessToken(graphHelper *graphhelper.GraphHelper) { token, err := graphHelper.GetUserToken() if err != nil { log.Panicf("Error getting user token: %v\n", err) } fmt.Printf("User token: %s", *token) fmt.Println() }
Build and run the app by running
go run graphtutorial
. Enter1
when prompted for an option. The application displays a URL and device code.Go Graph Tutorial Please choose one of the following options: 0. Exit 1. Display access token 2. List my inbox 3. Send mail 4. Make a Graph call 1 To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code RB2RUD56D to authenticate.
Open a browser and browse to the URL displayed. Enter the provided code and sign in.
Important
Be mindful of any existing Microsoft 365 accounts that are logged into your browser when browsing to
https://microsoft.com/devicelogin
. Use browser features such as profiles, guest mode, or private mode to ensure that you authenticate as the account you intend to use for testing.Once completed, return to the application to see the access token.
Tip
For validation and debugging purposes only, you can decode user access tokens (for work or school accounts only) using Microsoft's online token parser at https://jwt.ms. This can be useful if you encounter token errors when calling Microsoft Graph. For example, verifying that the
scp
claim in the token contains the expected Microsoft Graph permission scopes.
Get user
In this section you will incorporate the Microsoft Graph into the application. For this application, you will use the Microsoft Graph SDK for Go to make calls to Microsoft Graph.
Add the following function to ./graphhelper/graphhelper.go.
func (g *GraphHelper) GetUser() (models.Userable, error) { query := users.UserItemRequestBuilderGetQueryParameters{ // Only request specific properties Select: []string{"displayName", "mail", "userPrincipalName"}, } return g.userClient.Me().Get(context.Background(), &users.UserItemRequestBuilderGetRequestConfiguration{ QueryParameters: &query, }) }
Replace the empty
greetUser
function in graphtutorial.go with the following.func greetUser(graphHelper *graphhelper.GraphHelper) { user, err := graphHelper.GetUser() if err != nil { log.Panicf("Error getting user: %v\n", err) } fmt.Printf("Hello, %s!\n", *user.GetDisplayName()) // For Work/school accounts, email is in Mail property // Personal accounts, email is in UserPrincipalName email := user.GetMail() if email == nil { email = user.GetUserPrincipalName() } fmt.Printf("Email: %s\n", *email) fmt.Println() }
If you run the app now, after you log in the app welcomes you by name.
Hello, Megan Bowen!
Email: MeganB@contoso.com
Code explained
Consider the code in the getUser
function. It's only a few lines, but there are some key details to notice.
Accessing 'me'
The function uses the userClient.Me
request builder, which builds a request to the Get user API. This API is accessible two ways:
GET /me
GET /users/{user-id}
In this case, the code will call the GET /me
API endpoint. This is a shortcut method to get the authenticated user without knowing their user ID.
Note
Because the GET /me
API endpoint gets the authenticated user, it is only available to apps that use user authentication. App-only authentication apps cannot access this endpoint.
Requesting specific properties
The function uses the Select
property in the request's query parameters to specify the set of properties it needs. This adds the $select query parameter to the API call.
Strongly-typed return type
The function returns a Userable
object deserialized from the JSON response from the API. Because the code uses Select
, only the requested properties will have values in the returned Userable
object. All other properties will have default values.
List inbox
In this section you will add the ability to list messages in the user's email inbox.
Add the following function to ./graphhelper/graphhelper.go.
func (g *GraphHelper) GetInbox() (models.MessageCollectionResponseable, error) { var topValue int32 = 25 query := users.ItemMailFoldersItemMessagesRequestBuilderGetQueryParameters{ // Only request specific properties Select: []string{"from", "isRead", "receivedDateTime", "subject"}, // Get at most 25 results Top: &topValue, // Sort by received time, newest first Orderby: []string{"receivedDateTime DESC"}, } return g.userClient.Me().MailFolders(). ByMailFolderId("inbox"). Messages(). Get(context.Background(), &users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration{ QueryParameters: &query, }) }
Replace the empty
listInbox
function in graphtutorial.go with the following.func listInbox(graphHelper *graphhelper.GraphHelper) { messages, err := graphHelper.GetInbox() if err != nil { log.Panicf("Error getting user's inbox: %v", err) } // Load local time zone // Dates returned by Graph are in UTC, use this // to convert to local location, err := time.LoadLocation("Local") if err != nil { log.Panicf("Error getting local timezone: %v", err) } // Output each message's details for _, message := range messages.GetValue() { fmt.Printf("Message: %s\n", *message.GetSubject()) fmt.Printf(" From: %s\n", *message.GetFrom().GetEmailAddress().GetName()) status := "Unknown" if *message.GetIsRead() { status = "Read" } else { status = "Unread" } fmt.Printf(" Status: %s\n", status) fmt.Printf(" Received: %s\n", (*message.GetReceivedDateTime()).In(location)) } // If GetOdataNextLink does not return nil, // there are more messages available on the server nextLink := messages.GetOdataNextLink() fmt.Println() fmt.Printf("More messages available? %t\n", nextLink != nil) fmt.Println() }
Run the app, sign in, and choose option 2 to list your inbox.
Please choose one of the following options: 0. Exit 1. Display access token 2. List my inbox 3. Send mail 4. Make a Graph call 2 Message: Updates from Ask HR and other communities From: Contoso Demo on Yammer Status: Read Received: 2021-12-30 04:54:54 -0500 EST Message: Employee Initiative Thoughts From: Patti Fernandez Status: Read Received: 2021-12-28 17:01:10 -0500 EST Message: Voice Mail (11 seconds) From: Alex Wilber Status: Unread Received: 2021-12-28 17:00:46 -0500 EST Message: Our Spring Blog Update From: Alex Wilber Status: Unread Received: 2021-12-28 16:49:46 -0500 EST Message: Atlanta Flight Reservation From: Alex Wilber Status: Unread Received: 2021-12-28 16:35:42 -0500 EST Message: Atlanta Trip Itinerary - down time From: Alex Wilber Status: Unread Received: 2021-12-28 16:22:04 -0500 EST ... More messages available? True
Code explained
Consider the code in the GetInbox
function.
Accessing well-known mail folders
The function uses the userClient.Me().MailFolders.ByMailFolderId("inbox").Messages()
request builder, which builds a request to the List messages API. Because it includes the ByMailFolderId("inbox")
request builder, the API will only return messages in the requested mail folder. In this case, because the inbox is a default, well-known folder inside a user's mailbox, it's accessible via its well-known name. Non-default folders are accessed the same way, by replacing the well-known name with the mail folder's ID property. For details on the available well-known folder names, see mailFolder resource type.
Accessing a collection
Unlike the GetUser
function from the previous section, which returns a single object, this method returns a collection of messages. Most APIs in Microsoft Graph that return a collection do not return all available results in a single response. Instead, they use paging to return a portion of the results while providing a method for clients to request the next "page".
Default page sizes
APIs that use paging implement a default page size. For messages, the default value is 10. Clients can request more (or less) by using the $top query parameter. In GetInbox
, this is accomplished with the Top
property in the query parameters.
Note
The value passed in Top
is an upper-bound, not an explicit number. The API returns a number of messages up to the specified value.
Getting subsequent pages
If there are more results available on the server, collection responses include an @odata.nextLink
property with an API URL to access the next page. The Go SDK exposes this as the GetOdataNextLink
method on collection page objects. If this method returns non-nil, there are more results available.
Sorting collections
The function uses the OrderBy
property in the query parameters to request results sorted by the time the message is received (receivedDateTime
property). It includes the DESC
keyword so that messages received more recently are listed first. This adds the $orderby query parameter to the API call.
Send mail
In this section you will add the ability to send an email message as the authenticated user.
Add the following function to ./graphhelper/graphhelper.go.
func (g *GraphHelper) SendMail(subject *string, body *string, recipient *string) error { // Create a new message message := models.NewMessage() message.SetSubject(subject) messageBody := models.NewItemBody() messageBody.SetContent(body) contentType := models.TEXT_BODYTYPE messageBody.SetContentType(&contentType) message.SetBody(messageBody) toRecipient := models.NewRecipient() emailAddress := models.NewEmailAddress() emailAddress.SetAddress(recipient) toRecipient.SetEmailAddress(emailAddress) message.SetToRecipients([]models.Recipientable{ toRecipient, }) sendMailBody := users.NewItemSendMailPostRequestBody() sendMailBody.SetMessage(message) // Send the message return g.userClient.Me().SendMail().Post(context.Background(), sendMailBody, nil) }
Replace the empty
sendMail
function in graphtutorial.go with the following.func sendMail(graphHelper *graphhelper.GraphHelper) { // Send mail to the signed-in user // Get the user for their email address user, err := graphHelper.GetUser() if err != nil { log.Panicf("Error getting user: %v", err) } // For Work/school accounts, email is in Mail property // Personal accounts, email is in UserPrincipalName email := user.GetMail() if email == nil { email = user.GetUserPrincipalName() } subject := "Testing Microsoft Graph" body := "Hello world!" err = graphHelper.SendMail(&subject, &body, email) if err != nil { log.Panicf("Error sending mail: %v", err) } fmt.Println("Mail sent.") fmt.Println() }
Run the app, sign in, and choose option 3 to send an email to yourself.
Please choose one of the following options: 0. Exit 1. Display access token 2. List my inbox 3. Send mail 4. Make a Graph call 3 Mail sent.
Note
If you are testing with a developer tenant from the Microsoft 365 Developer Program, the email you send might not be delivered, and you might receive a non-delivery report. If this happens to you, please contact support via the Microsoft 365 admin center.
To verify the message was received, choose option 2 to list your inbox.
Code explained
Consider the code in the SendMail
function.
Sending mail
The function uses the userClient.Me().SendMail()
request builder, which builds a request to the Send mail API. The request builder takes a Message
object representing the message to send.
Creating objects
Unlike the previous calls to Microsoft Graph that only read data, this call creates data. To do this with the client library you create an instance of the class representing the data (in this case, models.Message
), set the desired properties, then send it in the API call. Because the call is sending data, the Post
method is used instead of Get
.
Optional: add your own code
In this section you will add your own Microsoft Graph capabilities to the application. This could be a code snippet from Microsoft Graph documentation or Graph Explorer, or code that you created. This section is optional.
Update the app
Add the following function to ./graphhelper/graphhelper.go.
func (g *GraphHelper) MakeGraphCall() error { // INSERT YOUR CODE HERE return nil }
Replace the empty
makeGraphCall
function in graphtutorial.go with the following.func makeGraphCall(graphHelper *graphhelper.GraphHelper) { err := graphHelper.MakeGraphCall() if err != nil { log.Panicf("Error making Graph call: %v", err) } }
Choose an API
Find an API in Microsoft Graph you'd like to try. For example, the Create event API. You can use one of the examples in the API documentation, or you can customize an API request in Graph Explorer and use the generated snippet.
Configure permissions
Check the Permissions section of the reference documentation for your chosen API to see which authentication methods are supported. Some APIs don't support app-only, or personal Microsoft accounts, for example.
- To call an API with user authentication (if the API supports user (delegated) authentication), add the required permission scope in .env (or .env.local).
- To call an API with app-only authentication see the app-only authentication tutorial.
Add your code
Copy your code into the MakeGraphCall
function in graphhelper.go. If you're copying a snippet from documentation or Graph Explorer, be sure to rename the GraphServiceClient
to userClient
.
Congratulations!
You've completed the Go Microsoft Graph tutorial. Now that you have a working app that calls Microsoft Graph, you can experiment and add new features.
- Learn how to use app-only authentication with the Microsoft Graph SDK for Go.
- Visit the Overview of Microsoft Graph to see all of the data you can access with Microsoft Graph.
Have an issue with this section? If so, please give us some feedback so we can improve this section.