/// <summary> /// Creates the connector client. /// </summary> /// <param name="serviceUrl">The service URL.</param> /// <param name="appCredentials">The application credentials for the bot.</param> /// <returns>Connector client instance.</returns> private IConnectorClient CreateConnectorClient(string serviceUrl, MicrosoftAppCredentials appCredentials = null) { string clientKey = $"{serviceUrl}{appCredentials?.MicrosoftAppId ?? string.Empty}"; return(_connectorClients.GetOrAdd(clientKey, (key) => { ConnectorClient connectorClient; if (appCredentials != null) { connectorClient = new ConnectorClient(new Uri(serviceUrl), appCredentials); } else { var emptyCredentials = (_channelProvider != null && _channelProvider.IsGovernment()) ? MicrosoftGovernmentAppCredentials.Empty : MicrosoftAppCredentials.Empty; connectorClient = new ConnectorClient(new Uri(serviceUrl), emptyCredentials); } if (_connectorClientRetryPolicy != null) { connectorClient.SetRetryPolicy(_connectorClientRetryPolicy); } return connectorClient; })); }
/// <summary> /// Gets the application credentials. App Credentials are cached so as to ensure we are not refreshing /// token every time. /// </summary> /// <param name="appId">The application identifier (AAD Id for the bot).</param> /// <param name="oAuthScope">The scope for the token, skills will use the Skill App Id. </param> /// <returns>App credentials.</returns> private async Task <AppCredentials> GetAppCredentialsAsync(string appId, string oAuthScope = null) { if (appId == null) { return(MicrosoftAppCredentials.Empty); } var cacheKey = $"{appId}{oAuthScope}"; if (_appCredentialMap.TryGetValue(cacheKey, out var appCredentials)) { return(appCredentials); } // If app credentials were provided, use them as they are the preferred choice moving forward if (_appCredentials != null) { // Cache the credentials for later use _appCredentialMap[cacheKey] = _appCredentials; return(_appCredentials); } // NOTE: we can't do async operations inside of a AddOrUpdate, so we split access pattern var appPassword = await _credentialProvider.GetAppPasswordAsync(appId).ConfigureAwait(false); appCredentials = _channelProvider != null && _channelProvider.IsGovernment() ? new MicrosoftGovernmentAppCredentials(appId, appPassword, _httpClient, _logger) : new MicrosoftAppCredentials(appId, appPassword, _httpClient, _logger, oAuthScope); // Cache the credentials for later use _appCredentialMap[cacheKey] = appCredentials; return(appCredentials); }
/// <summary> /// Validates the authentication header of an incoming request. /// </summary> /// <param name="authHeader">The authentication header to validate.</param> /// <param name="credentials">The bot's credential provider.</param> /// <param name="channelProvider">The bot's channel service provider.</param> /// <param name="channelId">The ID of the channel that sent the request.</param> /// <param name="serviceUrl">The service URL for the activity.</param> /// <param name="httpClient">The HTTP client.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <remarks>If the task completes successfully, the result contains the claims-based /// identity for the request.</remarks> public static async Task <ClaimsIdentity> ValidateAuthHeader(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, string serviceUrl = null, HttpClient httpClient = null) { if (string.IsNullOrEmpty(authHeader)) { throw new ArgumentNullException(nameof(authHeader)); } bool usingEmulator = EmulatorValidation.IsTokenFromEmulator(authHeader); if (usingEmulator) { return(await EmulatorValidation.AuthenticateEmulatorToken(authHeader, credentials, channelProvider, httpClient ?? _httpClient, channelId)); } else if (channelProvider == null || channelProvider.IsPublicAzure()) { // No empty or null check. Empty can point to issues. Null checks only. if (serviceUrl != null) { return(await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient ?? _httpClient, channelId)); } else { return(await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, httpClient ?? _httpClient, channelId)); } } else if (channelProvider.IsGovernment()) { return(await GovernmentChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient ?? _httpClient, channelId).ConfigureAwait(false)); } else { return(await EnterpriseChannelValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient ?? _httpClient, channelId).ConfigureAwait(false)); } }
/// <summary> /// Authenticates the auth header token from the request. /// </summary> private static async Task <ClaimsIdentity> AuthenticateTokenAsync(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, AuthenticationConfiguration authConfig, string serviceUrl, HttpClient httpClient) { if (SkillValidation.IsSkillToken(authHeader)) { return(await SkillValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false)); } if (EmulatorValidation.IsTokenFromEmulator(authHeader)) { return(await EmulatorValidation.AuthenticateEmulatorToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false)); } if (channelProvider == null || channelProvider.IsPublicAzure()) { // No empty or null check. Empty can point to issues. Null checks only. if (serviceUrl != null) { return(await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false)); } return(await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, httpClient, channelId, authConfig).ConfigureAwait(false)); } if (channelProvider.IsGovernment()) { return(await GovernmentChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false)); } return(await EnterpriseChannelValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false)); }
/// <summary> /// Validates that the incoming Auth Header is a token sent from a bot to a skill or from a skill to a bot. /// </summary> /// <param name="authHeader">The raw HTTP header in the format: "Bearer [longString]".</param> /// <param name="credentials">The user defined set of valid credentials, such as the AppId.</param> /// <param name="channelProvider">The channelService value that distinguishes public Azure from US Government Azure.</param> /// <param name="httpClient"> /// Authentication of tokens requires calling out to validate Endorsements and related documents. The /// HttpClient is used for making those calls. Those calls generally require TLS connections, which are expensive to /// setup and teardown, so a shared HttpClient is recommended. /// </param> /// <param name="channelId">The ID of the channel to validate.</param> /// <param name="authConfig">The authentication configuration.</param> /// <returns>A <see cref="ClaimsIdentity"/> instance if the validation is successful.</returns> #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods (can't change this without breaking binary compat) public static async Task <ClaimsIdentity> AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) #pragma warning restore VSTHRD200 // Use "Async" suffix for async methods { if (authConfig == null) { throw new ArgumentNullException(nameof(authConfig)); } var openIdMetadataUrl = channelProvider != null && channelProvider.IsGovernment() ? GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; var tokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuers = new[] { "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Auth v3.1, 1.0 token "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Auth v3.1, 2.0 token "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth v3.2, 1.0 token "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth v3.2, 2.0 token "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", // Auth for US Gov, 1.0 token "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", // Auth for US Gov, 2.0 token "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth for US Gov, 1.0 token "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth for US Gov, 2.0 token }, ValidateAudience = false, // Audience validation takes place manually in code. ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5), RequireSignedTokens = true }; // Add allowed token issuers from configuration (if present) if (authConfig.ValidTokenIssuers != null && authConfig.ValidTokenIssuers.Any()) { var validIssuers = tokenValidationParameters.ValidIssuers.ToList(); validIssuers.AddRange(authConfig.ValidTokenIssuers); tokenValidationParameters.ValidIssuers = validIssuers; } var tokenExtractor = new JwtTokenExtractor( httpClient, tokenValidationParameters, openIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements).ConfigureAwait(false); await ValidateIdentityAsync(identity, credentials).ConfigureAwait(false); return(identity); }
/// <summary> /// Validates that the incoming Auth Header is a token sent from a bot to a skill or from a skill to a bot. /// </summary> /// <param name="authHeader">The raw HTTP header in the format: "Bearer [longString]".</param> /// <param name="credentials">The user defined set of valid credentials, such as the AppId.</param> /// <param name="channelProvider">The channelService value that distinguishes public Azure from US Government Azure.</param> /// <param name="httpClient"> /// Authentication of tokens requires calling out to validate Endorsements and related documents. The /// HttpClient is used for making those calls. Those calls generally require TLS connections, which are expensive to /// setup and teardown, so a shared HttpClient is recommended. /// </param> /// <param name="channelId">The ID of the channel to validate.</param> /// <param name="authConfig">The authentication configuration.</param> /// <returns>A <see cref="ClaimsIdentity"/> instance if the validation is successful.</returns> public static async Task <ClaimsIdentity> AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) { if (authConfig == null) { throw new ArgumentNullException(nameof(authConfig)); } var openIdMetadataUrl = channelProvider != null && channelProvider.IsGovernment() ? GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; var tokenExtractor = new JwtTokenExtractor( httpClient, _tokenValidationParameters, openIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements).ConfigureAwait(false); await ValidateIdentity(identity, credentials).ConfigureAwait(false); return(identity); }
/// <summary> /// Validate the incoming Auth Header as a token sent from the Bot Framework Emulator. /// </summary> /// <param name="authHeader">The raw HTTP header in the format: "Bearer [longString]".</param> /// <param name="credentials">The user defined set of valid credentials, such as the AppId.</param> /// <param name="channelProvider">The channelService value that distinguishes public Azure from US Government Azure.</param> /// <param name="httpClient">Authentication of tokens requires calling out to validate Endorsements and related documents. The /// HttpClient is used for making those calls. Those calls generally require TLS connections, which are expensive to /// setup and teardown, so a shared HttpClient is recommended.</param> /// <param name="channelId">The ID of the channel to validate.</param> /// <param name="authConfig">The authentication configuration.</param> /// <returns> /// A valid ClaimsIdentity. /// </returns> /// <remarks> /// A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. /// </remarks> #pragma warning disable UseAsyncSuffix // Use Async suffix (can't change this without breaking binary compat) public static async Task <ClaimsIdentity> AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) #pragma warning restore UseAsyncSuffix // Use Async suffix { if (authConfig == null) { throw new ArgumentNullException(nameof(authConfig)); } var openIdMetadataUrl = (channelProvider != null && channelProvider.IsGovernment()) ? GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; var tokenExtractor = new JwtTokenExtractor( httpClient, ToBotFromEmulatorTokenValidationParameters, openIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements).ConfigureAwait(false); if (identity == null) { // No valid identity. Not Authorized. throw new UnauthorizedAccessException("Invalid Identity"); } if (!identity.IsAuthenticated) { // The token is in some way invalid. Not Authorized. throw new UnauthorizedAccessException("Token Not Authenticated"); } // Now check that the AppID in the claimset matches // what we're looking for. Note that in a multi-tenant bot, this value // comes from developer code that may be reaching out to a service, hence the // Async validation. Claim versionClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.VersionClaim); if (versionClaim == null) { throw new UnauthorizedAccessException("'ver' claim is required on Emulator Tokens."); } string tokenVersion = versionClaim.Value; string appID = string.Empty; // The Emulator, depending on Version, sends the AppId via either the // appid claim (Version 1) or the Authorized Party claim (Version 2). if (string.IsNullOrWhiteSpace(tokenVersion) || tokenVersion == "1.0") { // either no Version or a version of "1.0" means we should look for // the claim in the "appid" claim. Claim appIdClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AppIdClaim); if (appIdClaim == null) { // No claim around AppID. Not Authorized. throw new UnauthorizedAccessException("'appid' claim is required on Emulator Token version '1.0'."); } appID = appIdClaim.Value; } else if (tokenVersion == "2.0") { // Emulator, "2.0" puts the AppId in the "azp" claim. Claim appZClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AuthorizedParty); if (appZClaim == null) { // No claim around AppID. Not Authorized. throw new UnauthorizedAccessException("'azp' claim is required on Emulator Token version '2.0'."); } appID = appZClaim.Value; } else { // Unknown Version. Not Authorized. throw new UnauthorizedAccessException($"Unknown Emulator Token version '{tokenVersion}'."); } if (!await credentials.IsValidAppIdAsync(appID).ConfigureAwait(false)) { throw new UnauthorizedAccessException($"Invalid AppId passed on token: {appID}"); } return(identity); }