Custom Teams Tabs
Authentication using Teams
Configure Azure for Authentication
For this example, we will also be authenticating with the Graph API. Our token generated from Teams is limited to what it can access from graph, so we will want to create another app that interacts with Microsoft graph itself. We just want to show how you can authenticate to the web application and make a secure call so our application will be using application permissions to access graph. We will only need User.Read.All for this. The process is similar to above, but will require a secret to be generated for the application. I have done this in a previous post here: https://www.fiveminutecoder.com/2021/03/creating-azure-document-queue-for.html
Create our tab in 5 minutes
dotnet new mvc --name OfficeEmployeeDirectory
- Microsoft.Identity.Web
- Microsoft.Identity.Web.UI
- Microsoft.IdentityModel.Clients.ActiveDirectory
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.AspNetCore.Authentication.AzureAD.UI
- Microsoft.Graph
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"SignInUrl": "https://localhost:5001/Signin/SigninStart",
"ReturnUrl": "https://localhost:5001/signin/signinend",
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "https://localhost:5001",
"ClientId": "api://{client id}",
"TenantId": "common",
"CallbackPath": "/signin/signinend",
"Audience": "{client id}",
"Scopes" : "access_as_user access_as_admin",
"AllowWebApiToBeAuthorizedByACL" : true
},
"DirectoryApp":{
"TenantId": "{tenant id}",
"ClientId": "{client id for graph app}",
"clientSecret": "{client secret for graph app}"
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//authentication starts here
services.AddAuthentication(options =>{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; //using JWT token auth
})
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd")) //auth will act like a web api, otherwise our app tries to popup another login screen which is blocked
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.AddControllersWithViews(options =>
{
//add authenticated user to secure the app
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddRazorPages()
.AddMicrosoftIdentityUI();
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OfficeEmployeeDirectory.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
namespace OfficeEmployeeDirectory.Controllers
{
//just controls the login views routing
[AllowAnonymous]
public class SigninController: Controller
{
private IConfiguration configuration;
public SigninController(IConfiguration Configuration)
{
configuration = Configuration;
}
public ActionResult SigninStart()
{
ViewBag.ReturnUrl = configuration.GetValue("ReturnUrl");
ViewBag.ClientId = configuration.GetSection("AzureAd").GetValue("Audience");
return View();
}
public ActionResult SigninEnd()
{
return View();
}
}
}
//calls the teams login
//javascript
let clientId = "@ViewBag.ClientId";
if (clientId != undefined && clientId != null && clientId !== '') {
microsoftTeams.initialize();
let state = _guid();
localStorage.setItem("simple.state", state);
localStorage.removeItem("simple.error");
// See https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-implicit
// for documentation on these query parameters
let queryParams = {
client_id: clientId,
response_type: "id_token token", //what we want returned
response_mode: "fragment",
resource: "https://graph.microsoft.com/", //resource we need access to
redirect_uri: "@ViewBag.ReturnUrl", //return url
nonce: _guid(),//unique value to reference in callback so our app can validate it was us making the call
state: state
};
let authorizeEndpoint =
"https://login.microsoftonline.com/common/oauth2/authorize?" +
toQueryString(queryParams);
window.location.assign(authorizeEndpoint);
}
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(
key + "=" + encodeURIComponent(queryParams[key])
);
}
return encodedQueryParams.join("&");
}
//Create a unique identifier to validate the callback
function _guid() {
let guidHolder = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
let hex = "0123456789abcdef";
let r = 0;
let guidResponse = "";
for (let i = 0; i < 36; i++) {
if (guidHolder[i] !== "-" && guidHolder[i] !== "4") {
// each x and y needs to be random
r = (Math.random() * 16) | 0;
}
if (guidHolder[i] === "x") {
guidResponse += hex[r];
} else if (guidHolder[i] === "y") {
// clock-seq-and-reserved first hex is filtered and remaining hex values are random
r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??
r |= 0x8; // set pos 3 to 1 as 1???
guidResponse += hex[r];
} else {
guidResponse += guidHolder[i];
}
}
return guidResponse;
}
//sign in attempt finished, will parse query strings and save tokens to object
//javascript
microsoftTeams.initialize();
localStorage.removeItem("simple.error");
let hashParams = getHashParameters();
if (hashParams["error"]) {
// Authentication/authorization failed
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure(hashParams["error"]); //notifies our main page of an issue
} else if (hashParams["access_token"]) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("simple.state");
if (expectedState !== hashParams["state"]) {
// State does not match, report error
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure("StateDoesNotMatch");
} else {
// Success -- return token information to the parent page
microsoftTeams.authentication.notifySuccess({
idToken: hashParams["id_token"],
accessToken: hashParams["access_token"],
tokenType: hashParams["token_type"],
expiresIn: hashParams["expires_in"]
});
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure("UnexpectedFailure");
}
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = {};
location.hash.substr(1).split("&").forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams[k] = v;
});
return hashParams;
}
//our secure endpoint
[HttpGet]
public async Task GetDirectory()
{
try
{
//pulls config data for our graph api
string tenantId = configuration.GetSection("DirectoryApp").GetValue("TenantId"); //realm
//some service account with graph api permissions
string clientId = configuration.GetSection("DirectoryApp").GetValue("ClientId");
//service account password
string clientSecret = configuration.GetSection("DirectoryApp").GetValue("ClientSecret");;
string[] scopes = new string[] {"https://graph.microsoft.com/.default" };
//creates a header for accessing our graph api
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri("https://login.microsoftonline.com/" + tenantId))
.Build();
//gets token
AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
//creates graph client
GraphServiceClient client = new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(
async (requestMessage) =>
{
//adds token to header
requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
}
));
//pulls all users who's account is enabled
var users = await client.Users.Request().Filter("AccountEnabled eq true").GetAsync();
return Json(users);
}
catch(Exception ex)
{
return BadRequest(ex.Message);
}
}
@{
ViewData["Title"] = "Employee Directory";
}
//styling
.card{
width:400px;
height:200px;
border:1px solid black;
margin-bottom: 20px;
padding: 5px;
}
Employee Directory
label id="jsonResponse" label
//script
//run our script when window loads
window.addEventListener("load", function(){
//load the microsoft teams SDK
microsoftTeams.initialize();
//params for our login method
authenticateParams = {
successCallback: function(result){
var token = result["idToken"];
var access_token = result["accessToken"];
GetEmployeeDirectory(token);
},
failureCallback: function(reason){
alert("failed: " + reason);
},
height: 200,
width: 200,
url: "@ViewBag.SignInUrl"
}
//start authentication process
microsoftTeams.authentication.authenticate(authenticateParams);
});
//success callback to our secure api
function GetEmployeeDirectory(token){
fetch("/Home/GetDirectory", {
method: "GET",
headers: {
"content-type": "application/json;odata=verbose",
"Accept": "application/json; odata=verbose",
"Authorization": "Bearer " + token
}
})
.then(response =>response.json())
.then(results =>{
// creates an html object to be rendered in our app
for(var i = 0; i < results.length; i++){
let div = document.createElement("div")
div.className = "card"
div.append(results[i].displayName);
div.append(document.createElement("br"))
div.append( results[i].mail)
div.append(document.createElement("br"))
div.append(results[i].businessPhones.length > 0 ? results[i].businessPhones[0] : document.createElement("label"))
document.getElementById("jsonResponse").append(div);
}
});
}
Testing our Tab in Microsoft Teams
ngrok http --host-header=rewrite 5000
No comments:
Post a Comment