One way to manipulate the Web version of Office365 is through the Microsoft Graph (Office365) API.
For example, with the Microsoft Graph (Office365) API, you can use the
- Send email from OutLook
- Excel Online file editing
- SharePoint file retrieval
The web version of Office365 can be used to perform various operations on behalf of users, such as
To use the API, it is necessary to obtain an access token using OAuth2.0, but this OAuth2.0 token-related work is quite complicated.
This section describes how to obtain a token for the Microsoft Graph (Office365) API and how to renew an expired token.
Square Needle
Since it was difficult to handle the OAuth2.0 on my own, I decided to use the AzureAD strategy passport-azure-ad
in Passport.js, which is created by Microsoft, based on a tutorial from Microsoft.
We also decided to use simple-oauth2
to update the token, based on the tutorial.
background knowledge
Preparation for application registration
This is true not only for the Microsoft Graph (Office365) API, but for OAuth2.0 in general, but in order to obtain a token, you must register the application that will obtain the token with the token issuer in advance.
For the Microsoft Graph (Office365) API, apps register with Azure Active Directory in Azure.
So, you must have an Azure account to register the application, or create one in advance if you do not have one.
There is a free version of Azure Active Directory, and the application can be registered for free, so there is no Azure fee for using it to obtain tokens.
OAuth2.0 flow and information required for apps
Roughly speaking, OAuth2.0 involves the following exchange.
- There is a login link in the app that takes the user to the login screen of the site that issues the token
- There, the user logs in and agrees to the token grant consent screen.
- If you agree, a token issuance request code will be issued and you will be redirected to the original application with this information
- The app sends the received token issuance request code to the site that issues the token, and receives the token as a response.
When transitioning from the login link
- Identify which app.App ID.
- Prove that the application is the right one.Secret."
- Where to return to the application from the consent screen.Redirect URL."
information, so those three pieces of information are needed to create the application.
App Registration
Application registration for Microsoft Graph (Office365) API is done through Azure Active Directory in the Azure Portal.
- [Azure]-[Azure Active Directory]-[Register App]-[+New Registration]
Set the "Redirect URI" to http://localhost:3000/auth/callback
on the registration screen.
Types of Accounts Supported
Here, you select the type of account to be supported, which is a bit complicated, so I will provide additional explanation.
First, Office365 is available for individuals and for businesses.
With Office365 for enterprises, a dedicated domain is assigned to each company, and the company adds and manages its own Office365 users.
Office365 for individuals, on the other hand, is managed by Microsoft for the domain, and individuals have their accounts added to Microsoft for use. This is what is called a Microsoft account.
The types of accounts to be set up in the application are
- Single Tenant
- multi-tenant
- Multi-tenant + Microsoft account
There are three types of
For example, if a company uses Office365, the Azure Active Directory contains Office365 users managed by the company.
Then, when registering the app in that Azure Active Directory, if the type of account supported by the app is "Single Tenant", You can limit the users that the app can get only to users in that Azure Active Directory (Office365 users).
On the other hand, if the account type is set to "Multi-tenant", beyond the Azure Active Directory users managed by your company, The application will also be able to acquire tokens for Office365 users from other companies.
Also, if you use "Multi-tenant + Microsoft account", as you might have guessed, the application will also allow you to get a token for your personal Microsoft account.
For example, if an individual user of Office365 wants to obtain a token for his/her own use, obtain an Azure account as appropriate, register an app, and then By setting the account type of that app to "Multi-tenant + Microsoft account", you can create an app that can obtain a personal Office365 token.
Note application information
Go to [Overview] of the application and note Application (client) ID
.
Secret Creation
[Go to "Certificates and Secrets" and click on +New Client Secret
under "Client Secrets".
Note that a secret is generated.
Application Creation
The app is now registered with Azure Active Directory. Next, actually create the app (web app).
In this case, we are using Express.js + Passport.js to create a simple website (web app) that allows users to log in using AzureAD authentication.
And "token obtained when logging in with AzureAD authentication" = "Microsoft Graph (Office365) API token".
Only the program part is described below. All the source code, including templates and how to use them, has been uploaded to this way (direction close to the speaker or towards the speaker).
server.ts
import * as dotenv from 'dotenv'; import * as express from 'express'; import * as session from 'express-session'; import * as path from 'path'; import * as cookieParser from 'cookie-parser'; import * as bodyParser from 'body-parser'; import * as passport from 'passport'; import { OIDCStrategy } from 'passport-azure-ad'; dotenv.config(); // ini const INI = { REDIRECT_URI: 'http://localhost:3000/auth/callback', AUTHORITY: 'https://login.microsoftonline.com/common', ID_METADATA: '/v2.0/.well-known/openid-configuration', AUTHORIZE_ENDPOINT: '/oauth2/v2.0/authorize', TOKEN_ENDPOINT: '/oauth2/v2.0/token', SCOPES: [ 'profile', 'offline_access', 'user.read', 'Files.Read', 'Sites.Read.All' ] }; // user database const user_db: any = {}; // user data -> user identifier passport.serializeUser((user: any, cb) => { cb(null, user.profile.oid); }); // user identifier -> user data passport.deserializeUser((id: string, cb) => { cb(null, user_db[id]); }); passport.use(new OIDCStrategy( { identityMetadata: `${INI.AUTHORITY}${INI.ID_METADATA}`, clientID: process.env.CLIENT_ID, responseType: 'code', responseMode: 'form_post', redirectUrl: INI.REDIRECT_URI, allowHttpForRedirectUrl: true, clientSecret: process.env.CLIENT_SECRET, validateIssuer: false, scope: INI.SCOPES, passReqToCallback: false }, (iss, sub, profile, access_token, refresh_token, done) => { try { if (profile.oid) { // create user data const user = { iss, sub, profile, access_token, refresh_token }; // register user data in user database // user identifier is profile.oid user_db[profile.oid] = user; return done(null, user); } return done(null, false); } catch (err) { return done(null, err); } } )); // express const app = express(); const PORT_NO = 3000; // session app.use(session({ secret: 'secret_value_here', resave: false, saveUninitialized: false })); // template app.set('views', path.dirname(__dirname) + '/view'); app.set('view engine', 'ejs'); app.engine('html', require('ejs').renderFile); // etc app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); // passport app.use(passport.initialize()); app.use(passport.session()); // route app.get('/', (req, res) => { res.render('index.html', { user: req.user }); }); app.get('/auth/signin', passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }) , (req, res) => { res.redirect('/'); } ); app.post('/auth/callback', passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }) , (req, res) => { res.redirect('/'); } ); app.get('/auth/signout', (req, res) => { // clear session database req.session.destroy(() => { }); res.redirect('/'); } ); app.listen(PORT_NO);
supplementary explanation
- Authentication part is done by
passport-azure-ad
. - Enter the "App ID", "Secret", and "Redirect URL" that you wrote down in the configuration file
.env
and use it. - The scope of the token can be changed using
SCOPES
. - Once authenticated, the user's OpenID is received, and the token is added to a simple DB
data_db
as user data using the OpenID as the key. - App account type was tested with "Multi-tenant + Microsoft account"
We have written a separate article on how to use Passport.js.
Application Execution
Running the application will launch a local web server and open http://localhost:3000
in a browser.
Click on signin
and you will be taken to the Office365 logon screen. Log on and agree to obtain a token.
When you return to the application via redirection, you will see the tokens (access token and refresh token) and user information.
When using the API, the access token is obtained using the refresh token obtained here, and the API is called with that access token.
This is followed by how to use the tokens and how to renew expired tokens.
Renewing expired tokens
The Microsoft Graph (Office365) API token has the following specifications
- Access token is valid for 1 hour
- When an access token expires, renew the access token with a refresh token
- Refresh tokens are valid for 90 days
- When you renew your access token with a refresh token, a new refresh token is also issued
In other words,Once a refresh token is acquired, it cannot be used permanently.
So, to allow the access token to be permanently updated with a refresh token obtained once, The refresh token should be changed over with a new refresh token that is obtained at the same time each time the access token is updated.
Based on the above, the following is a sample of updating an access token and retrieving user information using the access token.
import * as dotenv from 'dotenv'; import * as fs from 'fs'; import * as simpleOauth2 from 'simple-oauth2'; import axios from 'axios'; dotenv.config(); const oauth2 = simpleOauth2.create({ client: { id: process.env.CLIENT_ID, secret: process.env.CLIENT_SECRET }, auth: { tokenHost: "https://login.microsoftonline.com/common", authorizePath: "/oauth2/v2.0/authorize", tokenPath: "/oauth2/v2.0/token" } }); // Save the token. const TOKEN_PATH = './data/token'; async function getToken() { let graphToken: any = { "access_token": "", "refresh_token": "", "expires_in": "" }; if (fs.existsSync(TOKEN_PATH)) { // If a token is stored, use it. graphToken = JSON.parse(fs.readFileSync(TOKEN_PATH).toString()); const accessToken = oauth2.accessToken.create(graphToken); if (!accessToken.expired()) { // Return access token if token has not expired return graphToken.access_token; } } else { // No tokens are saved, so use the first refresh token you set. graphToken.refresh_token = process.env.REFRESH_TOKEN; } // Update token const accessToken = oauth2.accessToken.create(graphToken); const token = await accessToken.refresh(); // Save the token graphToken = token.token; fs.writeFileSync(TOKEN_PATH, JSON.stringify(graphToken)); // Return access token return graphToken.access_token; } // Graph API Testing async function testGetMe(token: string) { const url = `https://graph.microsoft.com/v1.0/me`; const res = await axios.request({ headers: { 'authorization': `Bearer ${token}` }, url, method: "GET" }); return res.data; } (async () => { try { const access_token = await getToken(); const user = await testGetMe(access_token); console.log(user); } catch (err) { console.log(err); } })();
supplementary explanation
- Using
simple-oauth2
to obtain an access token from a refresh token - For the first time, the access token is obtained from the refresh token obtained from the web app
- Refreshing the token returns both an access token and a refresh token
- The updated tokens are saved as a file in
data/token
. - The second migration uses the saved tokens
- (We use "2.2.1" because an error occurs if the version of
simple-oauth2
is 3 or higher. - As a sample of token usage, we use
testGetMe()
to obtain our own information.
Impressions, etc.
Since the token we have obtained is for a single Office365 user, the scope of privileges is limited to what that user can do. In other words, you cannot see other users' emails.
How to obtain a token as an administrator application is described in a separate article.
It may seem exaggerated since we are setting up a web server, but we need to get redirected after consent, so we need to set up a web server.
Reference Articles
- https://qiita.com/r-wakatsuki/items/3c84fff0471c658a99c1
- https://docs.microsoft.com/ja-jp/azure/active-directory/develop/active-directory-configurable-token-lifetimes
- https://docs.microsoft.com/ja-jp/graph/tutorials/node