Skip to content

Commit

Permalink
V1 Delta Roster Sample Update using Microsoft Communications SDK 1.2.…
Browse files Browse the repository at this point in the history
…0.9212 (#684)

* Add the Sample for V1 Delta Roster

* Namespace changes for V1 nuget

* Deployment issue fix

---------

Co-authored-by: Samarth Asthana <[email protected]>
  • Loading branch information
samarthasthana and Samarth Asthana authored Dec 11, 2023
1 parent f657ea3 commit e7f61c2
Show file tree
Hide file tree
Showing 46 changed files with 4,900 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// <copyright file="AuthenticationProvider.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// </copyright>

// THIS CODE HAS NOT BEEN TESTED RIGOROUSLY.USING THIS CODE IN PRODUCTION ENVIRONMENT IS STRICTLY NOT RECOMMENDED.
// THIS SAMPLE IS PURELY FOR DEMONSTRATION PURPOSES ONLY.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
namespace Sample.Common.Authentication
{
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph.Communications.Client.Authentication;
using Microsoft.Graph.Communications.Common;
using Microsoft.Graph.Communications.Common.Telemetry;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

/// <summary>
/// The authentication provider for this bot instance.
/// </summary>
/// <seealso cref="IRequestAuthenticationProvider" />
public class AuthenticationProvider : ObjectRoot, IRequestAuthenticationProvider
{
/// <summary>
/// The application name.
/// </summary>
private readonly string appName;

/// <summary>
/// The application identifier.
/// </summary>
private readonly string appId;

/// <summary>
/// The application secret.
/// </summary>
private readonly string appSecret;

/// <summary>
/// The open ID configuration refresh interval.
/// </summary>
private readonly TimeSpan openIdConfigRefreshInterval = TimeSpan.FromHours(2);

/// <summary>
/// The previous update timestamp for OpenIdConfig.
/// </summary>
private DateTime prevOpenIdConfigUpdateTimestamp = DateTime.MinValue;

/// <summary>
/// The open identifier configuration.
/// </summary>
private OpenIdConnectConfiguration openIdConfiguration;

/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationProvider" /> class.
/// </summary>
/// <param name="appName">The application name.</param>
/// <param name="appId">The application identifier.</param>
/// <param name="appSecret">The application secret.</param>
/// <param name="logger">The logger.</param>
public AuthenticationProvider(string appName, string appId, string appSecret, IGraphLogger logger)
: base(logger.NotNull(nameof(logger)).CreateShim(nameof(AuthenticationProvider)))
{
this.appName = appName.NotNullOrWhitespace(nameof(appName));
this.appId = appId.NotNullOrWhitespace(nameof(appId));
this.appSecret = appSecret.NotNullOrWhitespace(nameof(appSecret));
}

/// <summary>
/// Authenticates the specified request message.
/// This method will be called any time there is an outbound request.
/// In this case we are using the Microsoft.IdentityModel.Clients.ActiveDirectory library
/// to stamp the outbound http request with the OAuth 2.0 token using an AAD application id
/// and application secret. Alternatively, this method can support certificate validation.
/// Note that this is only for demonstration purpose. Since acquiring token can be a costly operation,
/// it is recomended to acquire token before hand and cache the token, instead of acquiring it for every incoming request.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="tenant">The tenant.</param>
/// <returns>
/// The <see cref="Task" />.
/// </returns>
public async Task AuthenticateOutboundRequestAsync(HttpRequestMessage request, string tenant)
{
const string schema = "Bearer";
const string replaceString = "{tenant}";
const string oauthV2TokenLink = "https://login.microsoftonline.com/{tenant}";
const string resource = "https://graph.microsoft.com/beta";

// If no tenant was specified, we craft the token link using the common tenant.
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
tenant = string.IsNullOrWhiteSpace(tenant) ? "common" : tenant;
var tokenLink = oauthV2TokenLink.Replace(replaceString, tenant);
var scopes = new string[] { $"{resource}/.default" };

this.GraphLogger.Info("AuthenticationProvider: Generating OAuth token.");
var app = ConfidentialClientApplicationBuilder.Create(this.appId)
.WithAuthority(tokenLink)
.WithClientSecret(this.appSecret)
.Build();

AuthenticationResult result;
try
{
result = await this.AcquireTokenWithRetryAsync(app, scopes, 3).ConfigureAwait(false);
}
catch (Exception ex)
{
this.GraphLogger.Error(ex, $"Failed to generate token for client: {this.appId}");
throw;
}

this.GraphLogger.Info($"AuthenticationProvider: Generated OAuth token. Expires in {result.ExpiresOn.Subtract(DateTimeOffset.UtcNow).TotalMinutes} minutes.");

request.Headers.Authorization = new AuthenticationHeaderValue(schema, result.AccessToken);
}

/// <summary>
/// Validates the request asynchronously.
/// This method will be called any time we have an incoming request.
/// Returning invalid result will trigger a Forbidden response.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>
/// The <see cref="RequestValidationResult" /> structure.
/// </returns>
public async Task<RequestValidationResult> ValidateInboundRequestAsync(HttpRequestMessage request)
{
var token = request?.Headers?.Authorization?.Parameter;
if (string.IsNullOrWhiteSpace(token))
{
return new RequestValidationResult { IsValid = false };
}

// Currently the service does not sign outbound request using AAD, instead it is signed
// with a private certificate. In order for us to be able to ensure the certificate is
// valid we need to download the corresponding public keys from a trusted source.
const string authDomain = "https://api.aps.skype.com/v1/.well-known/OpenIdConfiguration";
if (this.openIdConfiguration == null || DateTime.Now > this.prevOpenIdConfigUpdateTimestamp.Add(this.openIdConfigRefreshInterval))
{
this.GraphLogger.Info("Updating OpenID configuration");

// Download the OIDC configuration which contains the JWKS
IConfigurationManager<OpenIdConnectConfiguration> configurationManager =
new ConfigurationManager<OpenIdConnectConfiguration>(
authDomain,
new OpenIdConnectConfigurationRetriever());
this.openIdConfiguration = await configurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false);

this.prevOpenIdConfigUpdateTimestamp = DateTime.Now;
}

// The incoming token should be issued by graph.
var authIssuers = new[]
{
"https://graph.microsoft.com",
"https://api.botframework.com",
};

// Configure the TokenValidationParameters.
// Aet the Issuer(s) and Audience(s) to validate and
// assign the SigningKeys which were downloaded from AuthDomain.
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidIssuers = authIssuers,
ValidAudience = this.appId,
IssuerSigningKeys = this.openIdConfiguration.SigningKeys,
};

ClaimsPrincipal claimsPrincipal;
try
{
// Now validate the token. If the token is not valid for any reason, an exception will be thrown by the method
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
claimsPrincipal = handler.ValidateToken(token, validationParameters, out _);
}

// Token expired... should somehow return 401 (Unauthorized)
// catch (SecurityTokenExpiredException ex)
// Tampered token
// catch (SecurityTokenInvalidSignatureException ex)
// Some other validation error
// catch (SecurityTokenValidationException ex)
catch (Exception ex)
{
// Some other error
this.GraphLogger.Error(ex, $"Failed to validate token for client: {this.appId}.");
return new RequestValidationResult() { IsValid = false };
}

const string ClaimType = "http://schemas.microsoft.com/identity/claims/tenantid";
var tenantClaim = claimsPrincipal.FindFirst(claim => claim.Type.Equals(ClaimType, StringComparison.Ordinal));

if (string.IsNullOrEmpty(tenantClaim?.Value))
{
// No tenant claim given to us. reject the request.
return new RequestValidationResult { IsValid = false };
}

request.Properties.Add(HttpConstants.HeaderNames.Tenant, tenantClaim.Value);
return new RequestValidationResult { IsValid = true, TenantId = tenantClaim.Value };
}

/// <summary>
/// Acquires the token and retries if failure occurs.
/// </summary>
/// <param name="app">The application.</param>
/// <param name="scopes">The resources.</param>
/// <param name="attempts">The attempts.</param>
/// <returns>
/// The <see cref="AuthenticationResult" />.
/// </returns>
private async Task<AuthenticationResult> AcquireTokenWithRetryAsync(IConfidentialClientApplication app, string[] scopes, int attempts)
{
while (true)
{
attempts--;

try
{
return await app.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (Exception)
{
if (attempts < 1)
{
throw;
}
}

await Task.Delay(1000).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// <copyright file="AuthenticationWrapper.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// </copyright>

namespace Sample.Common.Authentication
{
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph.Communications.Client.Authentication;
using Microsoft.Graph.Communications.Common;
using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;

/// <summary>
/// A wrapper for the <see cref="IRequestAuthenticationProvider"/>
/// that maps to the <see cref="IAuthenticationProvider"/>.
/// </summary>
/// <seealso cref="IRequestAuthenticationProvider" />
/// <seealso cref="IAuthenticationProvider" />
public class AuthenticationWrapper : IRequestAuthenticationProvider, IAuthenticationProvider
{
private readonly IRequestAuthenticationProvider authenticationProvider;
private readonly string tenant;

/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationWrapper"/> class.
/// </summary>
/// <param name="authenticationProvider">The authentication provider.</param>
/// <param name="tenant">The tenant.</param>
public AuthenticationWrapper(IRequestAuthenticationProvider authenticationProvider, string tenant = null)
{
this.authenticationProvider = authenticationProvider.NotNull(nameof(authenticationProvider));
this.tenant = tenant;
}

/// <inheritdoc />
public Task AuthenticateOutboundRequestAsync(HttpRequestMessage request, string tenant)
{
return this.authenticationProvider.AuthenticateOutboundRequestAsync(request, tenant);
}

/// <inheritdoc />
public Task<RequestValidationResult> ValidateInboundRequestAsync(HttpRequestMessage request)
{
return this.authenticationProvider.ValidateInboundRequestAsync(request);
}

/// <inheritdoc />
public Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}
}
Loading

0 comments on commit e7f61c2

Please sign in to comment.