Build Ruby on Rails apps with Microsoft Graph
This tutorial teaches you how to build a Ruby on Rails web app that uses the Microsoft Graph API to retrieve calendar information for a user.
Tip
If you prefer to just download the completed tutorial, you can download it in two ways.
- Download the Ruby quick start to get working code in minutes.
- Download or clone the GitHub repository.
Prerequisites
Before you start this tutorial, you should have the following tools installed on your development machine.
You should also have 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.
- You can sign up for the Office 365 Developer Program to get a free Office 365 subscription.
Note
This tutorial was written with the following versions of the required tools. The steps in this guide may work with other versions, but that has not been tested.
- Ruby version 2.6.6.
- SQLite3 version 3.31.1
- Node.js version 14.15.0
- Yarn version 1.22.0
Feedback
Please provide any feedback on this tutorial in the GitHub repository.
Create a Ruby on Rails web app
In this exercise you will use Ruby on Rails to build a web app.
If you don't already have Rails installed, you can install it from your command-line interface (CLI) with the following command.
gem install rails -v 6.0.3.4Open your CLI, navigate to a directory where you have rights to create files, and run the following command to create a new Rails app.
rails new graph-tutorialNavigate to this new directory and enter the following command to start a local web server.
rails serverOpen your browser and navigate to
http://localhost:3000. If everything is working, you will see a "Yay! You're on Rails!" message. If you don't see that message, check the Rails getting started guide.
Install gems
Before moving on, install some additional gems that you will use later:
- omniauth-oauth2 for handling sign-in and OAuth token flows.
- omniauth-rails_csrf_protection for adding CSRF protection to OmniAuth.
- httparty for making calls to Microsoft Graph.
- activerecord-session_store for storing sessions in the database.
Open ./Gemfile and add the following lines.
# OAuth gem 'omniauth-oauth2', '~> 1.7' # OmniAuth CSRF protection gem 'omniauth-rails_csrf_protection', '~> 0.1.2' # REST calls to Microsoft Graph gem 'httparty', '~> 0.18.1' # Session storage in database gem 'activerecord-session_store', '~> 1.1.3'In your CLI, run the following command.
bundle installIn your CLI, run the following commands to configure the database for storing sessions.
rails generate active_record:session_migration rake db:migrateCreate a new file called
session_store.rbin the ./config/initializers directory, and add the following code.Rails.application.config.session_store :active_record_store, :key => '_graph_app_session'
Design the app
In this section you'll create the basic UI for the app.
Open ./app/views/layouts/application.html.erb and replace its contents with the following.
<!DOCTYPE html> <html> <head> <title>Ruby Graph Tutorial</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <link rel="shortcut icon" href="favicon.png"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css"/> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <%= link_to "Ruby Graph Tutorial", root_path, class: "navbar-brand" %> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <%= link_to "Home", root_path, class: "nav-link#{' active' if controller.controller_name == 'home'}" %> </li> <% if @user_name %> <li class="nav-item" data-turbolinks="false"> <%= link_to "Calendar", "/calendar", class: "nav-link#{' active' if controller.controller_name == 'calendar'}" %> </li> <% end %> </ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item"> <a class="nav-link external-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank"> <i class="ms-Icon ms-Icon--NavigateExternalInline mr-1"></i>Docs </a> </li> <% if @user_name %> <li class="nav-item dropdown"> <a class="nav-link avatar-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"> <% if @user_avatar %> <img src=<%= @user_avatar %> class="rounded-circle align-self-center mr-2 profile-photo"> <% else %> <%= image_tag "no-profile-photo.png", class: "rounded-circle align-self-center mr-2 profile-photo" %> <% end %> </a> <div class="dropdown-menu dropdown-menu-right"> <h5 class="dropdown-item-text mb-0"><%= @user_name %></h5> <p class="dropdown-item-text text-muted mb-0"><%= @user_email %></p> <div class="dropdown-divider"></div> <%= link_to "Sign Out", {:controller => :auth, :action => :signout}, :class => "dropdown-item" %> </div> </li> <% else %> <li class="nav-item"> <%= link_to "Sign In", "/auth/microsoft_graph_auth", method: :post, class: "nav-link" %> </li> <% end %> </ul> </div> </div> </nav> <main role="main" class="container"> <% if @errors %> <% @errors.each do |error| %> <div class="alert alert-danger" role="alert"> <p class="mb-3"><%= error[:message] %></p> <%if error[:debug] %> <pre class="alert-pre border bg-light p-2"><code><%= error[:debug] %></code></pre> <% end %> </div> <% end %> <% end %> <%= yield %> </main> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </body> </html>This code adds Bootstrap for simple styling, and Fabric Core for some simple icons. It also defines a global layout with a nav bar.
Open ./app/assets/stylesheets/application.css and add the following to the end of the file.
body { padding-top: 4.5rem; } .alert-pre { word-wrap: break-word; word-break: break-all; white-space: pre-wrap; } .external-link { padding-top: 6px; } .avatar-link { padding-top: 4px; padding-bottom: 4px; } .profile-photo { width: 32px; }Generate a home page controller with the following command.
rails generate controller Home indexConfigure the
indexaction on theHomecontroller as the default page for the app. Open ./config/routes.rb and replace its contents with the followingRails.application.routes.draw do get 'home/index' root 'home#index' # Add future routes here endOpen ./app/view/home/index.html.erb and replace its contents with the following.
<div class="jumbotron"> <h1>Ruby Graph Tutorial</h1> <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from Ruby</p> <% if @user_name %> <h4>Welcome <%= @user_name %>!</h4> <p>Use the navigation bar at the top of the page to get started.</p> <% else %> <%= link_to "Click here to sign in", "/auth/microsoft_graph_auth", method: :post, class: "btn btn-primary btn-large" %> <% end %> </div>Add a PNG file named no-profile-photo.png in the ./app/assets/images directory.
Save all of your changes and restart the server. Now, the app should look very different.

Register the app in the portal
In this exercise, you will create a new Azure AD web application registration using the Azure Active Directory admin center.
Open a browser and navigate to the Azure Active Directory admin center. Login using a personal account (aka: Microsoft Account) or Work or School Account.
Select Azure Active Directory in the left-hand navigation, then select App registrations under Manage.

Select New registration. On the Register an application page, set the values as follows.
- Set Name to
Ruby Graph Tutorial. - Set Supported account types to Accounts in any organizational directory and personal Microsoft accounts.
- Under Redirect URI, set the first drop-down to
Weband set the value tohttp://localhost:3000/auth/microsoft_graph_auth/callback.

- Set Name to
Select Register. On the Ruby Graph Tutorial page, copy the value of the Application (client) ID and save it, you will need it in the next step.

Select Certificates & secrets under Manage. Select the New client secret button. Enter a value in Description and select one of the options for Expires and select Add.

Copy the client secret value before you leave this page. You will need it in the next step.
Important
This client secret is never shown again, so make sure you copy it now.

Add Azure AD authentication
In this exercise 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 omniauth-oauth2 gem into the application, and create a custom OmniAuth strategy.
Create a separate file to hold your app ID and secret. Create a new file called
oauth_environment_variables.rbin the ./config folder, and add the following code.ENV['AZURE_APP_ID'] = 'YOUR_APP_ID_HERE' ENV['AZURE_APP_SECRET'] = 'YOUR_APP_SECRET_HERE' ENV['AZURE_SCOPES'] = 'openid profile email offline_access user.read mailboxsettings.read calendars.readwrite'Replace
YOUR_APP_ID_HEREwith the application ID from the Application Registration Portal, and replaceYOUR_APP_SECRET_HEREwith the password you generated.Important
If you're using source control such as git, now would be a good time to exclude the
oauth_environment_variables.rbfile from source control to avoid inadvertently leaking your app ID and password.Open ./config/environment.rb and add the following code before the
Rails.application.initialize!line.# Load the Rails application. require_relative 'application' # Load OAuth settings oauth_environment_variables = File.join(Rails.root, 'config', 'oauth_environment_variables.rb') load(oauth_environment_variables) if File.exist?(oauth_environment_variables) # Initialize the Rails application. Rails.application.initialize!
Setup OmniAuth
You've already installed the omniauth-oauth2 gem, but in order to make it work with the Azure OAuth endpoints, you need to create an OAuth2 strategy. This is a Ruby class that defines the parameters for making OAuth requests to the Azure provider.
Create a new file called
microsoft_graph_auth.rbin the ./lib`** folder, and add the following code.require 'omniauth-oauth2' module OmniAuth module Strategies # Implements an OmniAuth strategy to get a Microsoft Graph # compatible token from Azure AD class MicrosoftGraphAuth < OmniAuth::Strategies::OAuth2 option :name, :microsoft_graph_auth DEFAULT_SCOPE = 'openid email profile User.Read'.freeze # Configure the Microsoft identity platform endpoints option :client_options, :site => 'https://login.microsoftonline.com', :authorize_url => '/common/oauth2/v2.0/authorize', :token_url => '/common/oauth2/v2.0/token' # Send the scope parameter during authorize option :authorize_options, [:scope] # Unique ID for the user is the id field uid { raw_info['id'] } # Get additional information after token is retrieved extra do { 'raw_info' => raw_info } end def raw_info # Get user profile information from the /me endpoint @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me?$select=displayName,mail,mailboxSettings,userPrincipalName').parsed end def authorize_params super.tap do |params| params['scope'.to_sym] = request.params['scope'] if request.params['scope'] params[:scope] ||= DEFAULT_SCOPE end end # Override callback URL # OmniAuth by default passes the entire URL of the callback, including # query parameters. Azure fails validation because that doesn't match the # registered callback. def callback_url options[:redirect_uri] || (full_host + script_name + callback_path) end end end endTake a moment to review what this code does.
- It sets the
client_optionsto specify the Microsoft identity platform endpoints. - It specifies that the
scopeparameter should be sent during the authorize phase. - It maps the
idproperty of the user as the unique ID for the user. - It uses the access token to retrieve the user's profile from Microsoft Graph to fill in the
raw_infohash. - It overrides the callback URL to ensure that it matches the registered callback in the app registration portal.
- It sets the
Create a new file called
omniauth_graph.rbin the ./config/initializers folder, and add the following code.require 'microsoft_graph_auth' Rails.application.config.middleware.use OmniAuth::Builder do provider :microsoft_graph_auth, ENV['AZURE_APP_ID'], ENV['AZURE_APP_SECRET'], :scope => ENV['AZURE_SCOPES'] endThis code will execute when the app starts. It loads up the OmniAuth middleware with the
microsoft_graph_authprovider, configured with the environment variables set in oauth_environment_variables.rb.
Implement sign-in
Now that the OmniAuth middleware is configured, you can move on to adding sign-in to the app.
Run the following command in your CLI to generate a controller for sign-in and sign-out.
rails generate controller AuthOpen ./app/controllers/auth_controller.rb. Add a callback method to the
AuthControllerclass. This method will be called by the OmniAuth middleware once the OAuth flow is complete.def callback # Access the authentication hash for omniauth data = request.env['omniauth.auth'] # Temporary for testing! render json: data.to_json endFor now all this does is render the hash provided by OmniAuth. You'll use this to verify that sign-in is working before moving on.
Add the routes to ./config/routes.rb.
# Add route for OmniAuth callback match '/auth/:provider/callback', :to => 'auth#callback', :via => [:get, :post]Start the server and browse to
https://localhost:3000. Click the sign-in button and you should be redirected tohttps://login.microsoftonline.com. Login with your Microsoft account and consent to the requested permissions. The browser redirects to the app, showing the hash generated by OmniAuth.{ "provider": "microsoft_graph_auth", "uid": "eb52b3b2-c4ac-4b4f-bacd-d5f7ece55df0", "info": { "name": null }, "credentials": { "token": "eyJ0eXAi...", "refresh_token": "OAQABAAA...", "expires_at": 1529517383, "expires": true }, "extra": { "raw_info": { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(displayName,mail,mailboxSettings,userPrincipalName)/$entity", "displayName": "Lynne Robbins", "mail": "LynneR@contoso.OnMicrosoft.com", "userPrincipalName": "LynneR@contoso.OnMicrosoft.com", "id": "d294e784-840e-4f9f-bb1e-95c0a75f2f18@2d18179c-4386-4cbd-8891-7fd867c4f62e", "mailboxSettings": { "archiveFolder": "AAMkAGI2...", "timeZone": "Pacific Standard Time", "delegateMeetingMessageDeliveryOptions": "sendToDelegateOnly", "dateFormat": "M/d/yyyy", "timeFormat": "h:mm tt", "automaticRepliesSetting": { "status": "disabled", "externalAudience": "all", "internalReplyMessage": "", "externalReplyMessage": "", "scheduledStartDateTime": { "dateTime": "2020-12-09T17:00:00.0000000", "timeZone": "UTC" }, "scheduledEndDateTime": { "dateTime": "2020-12-10T17:00:00.0000000", "timeZone": "UTC" } }, "language": { "locale": "en-US", "displayName": "English (United States)" }, "workingHours": { "daysOfWeek": [ "monday", "tuesday", "wednesday", "thursday", "friday" ], "startTime": "08:00:00.0000000", "endTime": "17:00:00.0000000", "timeZone": { "name": "Pacific Standard Time" } } } } } }
Storing the tokens
Now that you can get tokens, it's time to implement a way to store them in the app. Since this is a sample app, for simplicity's sake, you'll store them in the session. A real-world app would use a more reliable secure storage solution, like a database.
Open ./app/controllers/application_controller.rb. Add the following method to the
ApplicationControllerclass.def save_in_session(auth_hash) # Save the token info session[:graph_token_hash] = auth_hash[:credentials] # Save the user's display name session[:user_name] = auth_hash.dig(:extra, :raw_info, :displayName) # Save the user's email address # Use the mail field first. If that's empty, fall back on # userPrincipalName session[:user_email] = auth_hash.dig(:extra, :raw_info, :mail) || auth_hash.dig(:extra, :raw_info, :userPrincipalName) # Save the user's time zone session[:user_timezone] = auth_hash.dig(:extra, :raw_info, :mailboxSettings, :timeZone) endThe method takes the OmniAuth hash as a parameter and extracts the relevant bits of information, then stores that in the session.
Add accessor functions to the
ApplicationControllerclass to retrieve the user name, email address, and access token back out of the session.def user_name session[:user_name] end def user_email session[:user_email] end def user_timezone session[:user_timezone] end def access_token session[:graph_token_hash][:token] endAdd the following code to the
ApplicationControllerclass that will run before any action is processed.before_action :set_user def set_user @user_name = user_name @user_email = user_email endThis method sets the variables that the layout (in application.html.erb) uses to show the user's information in the nav bar. By adding it here, you don't have to add this code in every single controller action. However, this will also run for actions in the
AuthController, which isn't optimal.Add the following code to the
AuthControllerclass in ./app/controllers/auth_controller.rb to skip the before action.skip_before_action :set_userUpdate the
callbackfunction in theAuthControllerclass to store the tokens in the session and redirect back to the main page. Replace the existingcallbackfunction with the following.def callback # Access the authentication hash for omniauth data = request.env['omniauth.auth'] # Save the data in the session save_in_session data redirect_to root_url end
Implement sign-out
Before you test this new feature, add a way to sign out.
Add the following action to the
AuthControllerclass.def signout reset_session redirect_to root_url endAdd this action to ./config/routes.rb.
get 'auth/signout'Restart the server and go through the sign-in process. You should end up back on the home page, but the UI should change to indicate that you are signed-in.

Click the user avatar in the top right corner to access the Sign Out link. Clicking Sign Out resets the session and returns you to the home page.

Refreshing tokens
If you look closely at the hash generated by OmniAuth, you'll notice there are two tokens in the hash: token and refresh_token. The value in token is the access token, which is sent in the Authorization header of API calls. This is the token that allows the app to access the Microsoft Graph on the user's behalf.
However, this token is short-lived. The token expires an hour after it is issued. This is where the refresh_token value becomes useful. The refresh token allows the app to request a new access token without requiring the user to sign in again. Update the token management code to implement token refresh.
Open ./app/controllers/application_controller.rb and add the following
requirestatements at the top:require 'microsoft_graph_auth' require 'oauth2'Add the following method to the
ApplicationControllerclass.def refresh_tokens(token_hash) oauth_strategy = OmniAuth::Strategies::MicrosoftGraphAuth.new( nil, ENV['AZURE_APP_ID'], ENV['AZURE_APP_SECRET'] ) token = OAuth2::AccessToken.new( oauth_strategy.client, token_hash[:token], :refresh_token => token_hash[:refresh_token] ) # Refresh the tokens new_tokens = token.refresh!.to_hash.slice(:access_token, :refresh_token, :expires_at) # Rename token key new_tokens[:token] = new_tokens.delete :access_token # Store the new hash session[:graph_token_hash] = new_tokens endThis method uses the oauth2 gem (a dependency of the
omniauth-oauth2gem) to refresh the tokens, and updates the session.Replace the current
access_tokenmethod with the following.def access_token token_hash = session[:graph_token_hash] # Get the expiry time - 5 minutes expiry = Time.at(token_hash[:expires_at] - 300) if Time.now > expiry # Token expired, refresh new_hash = refresh_tokens token_hash new_hash[:token] else token_hash[:token] end endInstead of just returning the token from the session, it will first check if it is close to expiration. If it is, then it will refresh before returning the token.
Get a calendar view
In this exercise you will incorporate the Microsoft Graph into the application. For this application, you will use the httparty gem to make calls to Microsoft Graph.
Create a Graph helper
Create a helper to manage all of your API calls. Run the following command in your CLI to generate the helper.
rails generate helper GraphOpen ./app/helpers/graph_helper.rb and replace the contents with the following.
require 'httparty' # Graph API helper methods module GraphHelper GRAPH_HOST = 'https://graph.microsoft.com'.freeze def make_api_call(method, endpoint, token, headers = nil, params = nil, payload = nil) headers ||= {} headers[:Authorization] = "Bearer #{token}" headers[:Accept] = 'application/json' params ||= {} case method.upcase when 'GET' HTTParty.get "#{GRAPH_HOST}#{endpoint}", :headers => headers, :query => params when 'POST' headers['Content-Type'] = 'application/json' HTTParty.post "#{GRAPH_HOST}#{endpoint}", :headers => headers, :query => params, :body => payload ? payload.to_json : nil else raise "HTTP method #{method.upcase} not implemented" end end end
Take a moment to review what this code does. It makes a simple GET or POST request via the httparty gem to the requested endpoint. It sends the access token in the Authorization header, and it includes any query parameters that are passed.
For example, to use the make_api_call method to do a GET to https://graph.microsoft.com/v1.0/me?$select=displayName, you could call it like so:
make_api_call 'GET', '/v1.0/me', access_token, { '$select': 'displayName' }
You'll build on this later as you implement more Microsoft Graph features into the app.
Get calendar events from Outlook
In your CLI, run the following command to add a new controller.
rails generate controller Calendar index newAdd the new route to ./config/routes.rb.
get 'calendar', :to => 'calendar#index'Add a new method to the Graph helper to get a calendar view. Open ./app/helpers/graph_helper.rb and add the following method to the
GraphHelpermodule.def get_calendar_view(token, start_datetime, end_datetime, timezone) get_events_url = '/v1.0/me/calendarview' headers = { 'Prefer' => "outlook.timezone=\"#{timezone}\"" } query = { 'startDateTime' => start_datetime.iso8601, 'endDateTime' => end_datetime.iso8601, '$select' => 'subject,organizer,start,end', '$orderby' => 'start/dateTime', '$top' => 50 } response = make_api_call 'GET', get_events_url, token, headers, query raise response.parsed_response.to_s || "Request returned #{response.code}" unless response.code == 200 response.parsed_response['value'] endConsider what this code is doing.
- The URL that will be called is
/v1.0/me/calendarview.- The
Prefer: outlook.timezoneheader causes the start and end times in the results to be adjusted to the user's time zone. - The
startDateTimeandendDateTimeparameters set the start and end of the view. - The
$selectparameter limits the fields returned for each events to just those the view will actually use. - The
$orderbyparameter sorts the results by start time. - The
$topparameter limits the results to 50 events.
- The
- For a successful response, it returns the array of items contained in the
valuekey.
- The URL that will be called is
Add a new method to the Graph helper to lookup an IANA time zone identifier based on a Windows time zone name. This is necessary because Microsoft Graph can return time zones as Windows time zone names, and the Ruby DateTime class requires IANA time zone identifiers.
TIME_ZONE_MAP = { 'Dateline Standard Time' => 'Etc/GMT+12', 'UTC-11' => 'Etc/GMT+11', 'Aleutian Standard Time' => 'America/Adak', 'Hawaiian Standard Time' => 'Pacific/Honolulu', 'Marquesas Standard Time' => 'Pacific/Marquesas', 'Alaskan Standard Time' => 'America/Anchorage', 'UTC-09' => 'Etc/GMT+9', 'Pacific Standard Time (Mexico)' => 'America/Tijuana', 'UTC-08' => 'Etc/GMT+8', 'Pacific Standard Time' => 'America/Los_Angeles', 'US Mountain Standard Time' => 'America/Phoenix', 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', 'Mountain Standard Time' => 'America/Denver', 'Central America Standard Time' => 'America/Guatemala', 'Central Standard Time' => 'America/Chicago', 'Easter Island Standard Time' => 'Pacific/Easter', 'Central Standard Time (Mexico)' => 'America/Mexico_City', 'Canada Central Standard Time' => 'America/Regina', 'SA Pacific Standard Time' => 'America/Bogota', 'Eastern Standard Time (Mexico)' => 'America/Cancun', 'Eastern Standard Time' => 'America/New_York', 'Haiti Standard Time' => 'America/Port-au-Prince', 'Cuba Standard Time' => 'America/Havana', 'US Eastern Standard Time' => 'America/Indianapolis', 'Turks And Caicos Standard Time' => 'America/Grand_Turk', 'Paraguay Standard Time' => 'America/Asuncion', 'Atlantic Standard Time' => 'America/Halifax', 'Venezuela Standard Time' => 'America/Caracas', 'Central Brazilian Standard Time' => 'America/Cuiaba', 'SA Western Standard Time' => 'America/La_Paz', 'Pacific SA Standard Time' => 'America/Santiago', 'Newfoundland Standard Time' => 'America/St_Johns', 'Tocantins Standard Time' => 'America/Araguaina', 'E. South America Standard Time' => 'America/Sao_Paulo', 'SA Eastern Standard Time' => 'America/Cayenne', 'Argentina Standard Time' => 'America/Buenos_Aires', 'Greenland Standard Time' => 'America/Godthab', 'Montevideo Standard Time' => 'America/Montevideo', 'Magallanes Standard Time' => 'America/Punta_Arenas', 'Saint Pierre Standard Time' => 'America/Miquelon', 'Bahia Standard Time' => 'America/Bahia', 'UTC-02' => 'Etc/GMT+2', 'Azores Standard Time' => 'Atlantic/Azores', 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', 'UTC' => 'Etc/GMT', 'GMT Standard Time' => 'Europe/London', 'Greenwich Standard Time' => 'Atlantic/Reykjavik', 'Sao Tome Standard Time' => 'Africa/Sao_Tome', 'Morocco Standard Time' => 'Africa/Casablanca', 'W. Europe Standard Time' => 'Europe/Berlin', 'Central Europe Standard Time' => 'Europe/Budapest', 'Romance Standard Time' => 'Europe/Paris', 'Central European Standard Time' => 'Europe/Warsaw', 'W. Central Africa Standard Time' => 'Africa/Lagos', 'Jordan Standard Time' => 'Asia/Amman', 'GTB Standard Time' => 'Europe/Bucharest', 'Middle East Standard Time' => 'Asia/Beirut', 'Egypt Standard Time' => 'Africa/Cairo', 'E. Europe Standard Time' => 'Europe/Chisinau', 'Syria Standard Time' => 'Asia/Damascus', 'West Bank Standard Time' => 'Asia/Hebron', 'South Africa Standard Time' => 'Africa/Johannesburg', 'FLE Standard Time' => 'Europe/Kiev', 'Israel Standard Time' => 'Asia/Jerusalem', 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', 'Sudan Standard Time' => 'Africa/Khartoum', 'Libya Standard Time' => 'Africa/Tripoli', 'Namibia Standard Time' => 'Africa/Windhoek', 'Arabic Standard Time' => 'Asia/Baghdad', 'Turkey Standard Time' => 'Europe/Istanbul', 'Arab Standard Time' => 'Asia/Riyadh', 'Belarus Standard Time' => 'Europe/Minsk', 'Russian Standard Time' => 'Europe/Moscow', 'E. Africa Standard Time' => 'Africa/Nairobi', 'Iran Standard Time' => 'Asia/Tehran', 'Arabian Standard Time' => 'Asia/Dubai', 'Astrakhan Standard Time' => 'Europe/Astrakhan', 'Azerbaijan Standard Time' => 'Asia/Baku', 'Russia Time Zone 3' => 'Europe/Samara', 'Mauritius Standard Time' => 'Indian/Mauritius', 'Saratov Standard Time' => 'Europe/Saratov', 'Georgian Standard Time' => 'Asia/Tbilisi', 'Volgograd Standard Time' => 'Europe/Volgograd', 'Caucasus Standard Time' => 'Asia/Yerevan', 'Afghanistan Standard Time' => 'Asia/Kabul', 'West Asia Standard Time' => 'Asia/Tashkent', 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', 'Pakistan Standard Time' => 'Asia/Karachi', 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', 'India Standard Time' => 'Asia/Calcutta', 'Sri Lanka Standard Time' => 'Asia/Colombo', 'Nepal Standard Time' => 'Asia/Katmandu', 'Central Asia Standard Time' => 'Asia/Almaty', 'Bangladesh Standard Time' => 'Asia/Dhaka', 'Omsk Standard Time' => 'Asia/Omsk', 'Myanmar Standard Time' => 'Asia/Rangoon', 'SE Asia Standard Time' => 'Asia/Bangkok', 'Altai Standard Time' => 'Asia/Barnaul', 'W. Mongolia Standard Time' => 'Asia/Hovd', 'North Asia Standard Time' => 'Asia/Krasnoyarsk', 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', 'Tomsk Standard Time' => 'Asia/Tomsk', 'China Standard Time' => 'Asia/Shanghai', 'North Asia East Standard Time' => 'Asia/Irkutsk', 'Singapore Standard Time' => 'Asia/Singapore', 'W. Australia Standard Time' => 'Australia/Perth', 'Taipei Standard Time' => 'Asia/Taipei', 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', 'Aus Central W. Standard Time' => 'Australia/Eucla', 'Transbaikal Standard Time' => 'Asia/Chita', 'Tokyo Standard Time' => 'Asia/Tokyo', 'North Korea Standard Time' => 'Asia/Pyongyang', 'Korea Standard Time' => 'Asia/Seoul', 'Yakutsk Standard Time' => 'Asia/Yakutsk', 'Cen. Australia Standard Time' => 'Australia/Adelaide', 'AUS Central Standard Time' => 'Australia/Darwin', 'E. Australia Standard Time' => 'Australia/Brisbane', 'AUS Eastern Standard Time' => 'Australia/Sydney', 'West Pacific Standard Time' => 'Pacific/Port_Moresby', 'Tasmania Standard Time' => 'Australia/Hobart', 'Vladivostok Standard Time' => 'Asia/Vladivostok', 'Lord Howe Standard Time' => 'Australia/Lord_Howe', 'Bougainville Standard Time' => 'Pacific/Bougainville', 'Russia Time Zone 10' => 'Asia/Srednekolymsk', 'Magadan Standard Time' => 'Asia/Magadan', 'Norfolk Standard Time' => 'Pacific/Norfolk', 'Sakhalin Standard Time' => 'Asia/Sakhalin', 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', 'Russia Time Zone 11' => 'Asia/Kamchatka', 'New Zealand Standard Time' => 'Pacific/Auckland', 'UTC+12' => 'Etc/GMT-12', 'Fiji Standard Time' => 'Pacific/Fiji', 'Chatham Islands Standard Time' => 'Pacific/Chatham', 'UTC+13' => 'Etc/GMT-13', 'Tonga Standard Time' => 'Pacific/Tongatapu', 'Samoa Standard Time' => 'Pacific/Apia', 'Line Islands Standard Time' => 'Pacific/Kiritimati' }.freeze def get_iana_from_windows(windows_tz_name) iana = TIME_ZONE_MAP[windows_tz_name] # If no mapping found, assume the supplied # value was already an IANA identifier iana || windows_tz_name endOpen ./app/controllers/calendar_controller.rb and replace its entire contents with the following.
# Calendar controller class CalendarController < ApplicationController include GraphHelper def index # Get the IANA identifier of the user's time zone time_zone = get_iana_from_windows(user_timezone) # Calculate the start and end of week in the user's time zone start_datetime = Date.today.beginning_of_week(:sunday).in_time_zone(time_zone).to_time end_datetime = start_datetime.advance(:days => 7) @events = get_calendar_view access_token, start_datetime, end_datetime, user_timezone || [] render json: @events rescue RuntimeError => e @errors = [ { :message => 'Microsoft Graph returned an error getting events.', :debug => e } ] end endRestart the server. Sign in and click the Calendar link in the nav bar. If everything works, you should see a JSON dump of events on the user's calendar.
Display the results
Now you can add HTML to display the results in a more user-friendly manner.
Open ./app/views/calendar/index.html.erb and replace its contents with the following.
<h1>Calendar</h1> <a href="/calendar/new" class="btn btn-light btn-sm mb-3">New event</a> <table class="table"> <thead> <tr> <th scope="col">Organizer</th> <th scope="col">Subject</th> <th scope="col">Start</th> <th scope="col">End</th> </tr> </thead> <tbody> <% @events.each do |event| %> <tr> <td><%= event['organizer']['emailAddress']['name'] %></td> <td><%= event['subject'] %></td> <td><%= event['start']['dateTime'].to_time(:utc).strftime('%-m/%-d/%Y %l:%M %p') %></td> <td><%= event['end']['dateTime'].to_time(:utc).strftime('%-m/%-d/%Y %l:%M %p') %></td> </tr> <% end %> </tbody> </table>That will loop through a collection of events and add a table row for each one.
Remove the
render json: @eventsline from theindexaction in ./app/controllers/calendar_controller.rb.Refresh the page and the app should now render a table of events.

Create a new event
In this section you will add the ability to create events on the user's calendar.
Open ./app/helpers/graph_helper.rb and add the following method to the Graph class.
def create_event(token, timezone, subject, start_datetime, end_datetime, attendees, body) create_event_url = '/v1.0/me/events' # Create an event object # https://docs.microsoft.com/graph/api/resources/event?view=graph-rest-1.0 new_event = { 'subject' => subject, 'start' => { 'dateTime' => start_datetime, 'timeZone' => timezone }, 'end' => { 'dateTime' => end_datetime, 'timeZone' => timezone } } unless attendees.empty? attendee_array = [] # Create an attendee object # https://docs.microsoft.com/graph/api/resources/attendee?view=graph-rest-1.0 attendees.each { |email| attendee_array.push({ 'type' => 'required', 'emailAddress' => { 'address' => email } }) } new_event['attendees'] = attendee_array end unless body.empty? # Create an itemBody object # https://docs.microsoft.com/graph/api/resources/itembody?view=graph-rest-1.0 new_event['body'] = { 'contentType' => 'text', 'content' => body } end response = make_api_call 'POST', create_event_url, token, nil, nil, new_event raise response.parsed_response.to_s || "Request returned #{response.code}" unless response.code == 201 endOpen ./app/controllers/calendar_controller and add the following route to the CalendarController class.
def create # Semicolon-delimited list, split to an array attendees = params[:ev_attendees].split(';') # Create the event create_event access_token, user_timezone, params[:ev_subject], params[:ev_start], params[:ev_end], attendees, params[:ev_body] # Redirect back to the calendar list redirect_to({ :action => 'index' }) rescue RuntimeError => e @errors = [ { :message => 'Microsoft Graph returned an error creating the event.', :debug => e } ] endOpen ./config/routes.rb and add the new route.
post 'calendar/new', :to => 'calendar#create'Open ./app/views/calendar/new.html.erb and replace its contents with the following.
<h1>New event</h1> <%= form_with url: "/calendar/new" do |form| %> <div class="form-group"> <%= form.label :ev_subject, "Subject" %> <%= form.text_field :ev_subject, class: "form-control", required: true %> </div> <div class="form-group"> <%= form.label :ev_attendees, "Attendees" %> <%= form.text_field :ev_attendees, class: "form-control", placeholder: "Separate multiple email addresses with a semicolon (';')" %> </div> <div class="form-row"> <div class="col"> <div class="form-group"> <%= form.label :ev_start, "Start" %> <%= form.datetime_local_field :ev_start, class: "form-control", required: true %> </div> </div> <div class="col"> <div class="form-group"> <%= form.label :ev_end, "End" %> <%= form.datetime_local_field :ev_end, class: "form-control", required: true %> </div> </div> </div> <div class="form-group mb-3"> <%= form.label :ev_body, "Body" %> <%= form.text_area :ev_body, class: "form-control", rows: "3" %> </div> <%= form.submit "Create", class: "btn btn-primary mr-2" %> <a class="btn btn-secondary" href="/calendar">Cancel</a> <% end %>Save your changes and refresh the app. On the Calendar page, select the New event button. Fill in the form and select Create to create a new event.

Congratulations!
You've completed the Ruby Microsoft Graph tutorial. Now that you have a working app that calls Microsoft Graph, you can experiment and add new features. Visit the Overview of Microsoft Graph to see all of the data you can access with Microsoft Graph.
Feedback
Please provide any feedback on this tutorial in the GitHub repository.
Have an issue with this section? If so, please give us some feedback so we can improve this section.