/// <summary> /// Validate the incoming Auth Header as a token sent from the Bot Framework Service. /// </summary> /// <remarks> /// A token issued by the Bot Framework emulator will FAIL this check. /// </remarks> /// <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="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> public static async Task <ClaimsIdentity> AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) { if (authConfig == null) { throw new ArgumentNullException(nameof(authConfig)); } var tokenExtractor = new JwtTokenExtractor( httpClient, ToBotFromChannelTokenValidationParameters, OpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements); if (identity == null) { // No valid identity. Not Authorized. throw new UnauthorizedAccessException(); } if (!identity.IsAuthenticated) { // The token is in some way invalid. Not Authorized. throw new UnauthorizedAccessException(); } // 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. // Look for the "aud" claim, but only if issued from the Bot Framework Claim audienceClaim = identity.Claims.FirstOrDefault( c => c.Issuer == AuthenticationConstants.ToBotFromChannelTokenIssuer && c.Type == AuthenticationConstants.AudienceClaim); if (audienceClaim == null) { // The relevant audience Claim MUST be present. Not Authorized. throw new UnauthorizedAccessException(); } // The AppId from the claim in the token must match the AppId specified by the developer. // In this case, the token is destined for the app, so we find the app ID in the audience claim. string appIdFromClaim = audienceClaim.Value; if (string.IsNullOrWhiteSpace(appIdFromClaim)) { // Claim is present, but doesn't have a value. Not Authorized. throw new UnauthorizedAccessException(); } if (!await credentials.IsValidAppIdAsync(appIdFromClaim)) { // The AppId is not valid. Not Authorized. throw new UnauthorizedAccessException($"Invalid AppId passed on token: {appIdFromClaim}"); } return(identity); }
private async Task <IdentityToken> TryAuthenticateAsync(JwtTokenExtractor toBotFromChannelExtractor, JwtTokenExtractor toBotFromEmulatorExtractor, string scheme, string token, CancellationToken cancellationToken) { // then auth is disabled if (await this.credentialProvider.IsAuthenticationDisabledAsync()) { return(new IdentityToken(true, null)); } ClaimsIdentity identity = null; string appId = null; identity = await toBotFromChannelExtractor.GetIdentityAsync(scheme, token); if (identity != null) { appId = toBotFromChannelExtractor.GetAppIdFromClaimsIdentity(identity); } // No identity? If we're allowed to, fall back to MSA // This code path is used by the emulator if (identity == null && !this.disableEmulatorTokens) { identity = await toBotFromEmulatorExtractor.GetIdentityAsync(scheme, token); if (identity != null) { appId = toBotFromEmulatorExtractor.GetAppIdFromEmulatorClaimsIdentity(identity); } } if (identity != null) { if (await credentialProvider.IsValidAppIdAsync(appId) == false) // keep context { // not valid appid, drop the identity identity = null; } else { var password = await credentialProvider.GetAppPasswordAsync(appId); // Keep context if (password != null) { // add password as claim so that it is part of ClaimsIdentity and accessible by ConnectorClient() identity.AddClaim(new Claim(ClaimsIdentityEx.AppPasswordClaim, password)); } } } if (identity != null) { return(new IdentityToken(true, identity)); } return(new IdentityToken(false, null)); }
internal async Task <IdentityToken> TryAuthenticateAsync(HttpRequestMessage request, CancellationToken token) { // then auth is disabled if (await this.credentialProvider.IsAuthenticationDisabledAsync()) { return(new IdentityToken(true, null)); } ClaimsIdentity identity = null; var tokenExtractor = GetTokenExtractor(); identity = await tokenExtractor.GetIdentityAsync(request); // No identity? If we're allowed to, fall back to MSA // This code path is used by the emulator if (identity == null && !this.disableEmulatorTokens) { tokenExtractor = new JwtTokenExtractor(JwtConfig.ToBotFromMSATokenValidationParameters, JwtConfig.ToBotFromMSAOpenIdMetadataUrl); identity = await tokenExtractor.GetIdentityAsync(request); } if (identity != null) { var appId = tokenExtractor.GetAppIdFromClaimsIdentity(identity); if (await credentialProvider.IsValidAppIdAsync(appId) == false) // keep context { // not valid appid, drop the identity identity = null; } else { var password = await credentialProvider.GetAppPasswordAsync(appId); // Keep context if (password != null) { // add password as claim so that it is part of ClaimsIdentity and accessible by ConnectorClient() identity.AddClaim(new Claim(ClaimsIdentityEx.AppPasswordClaim, password)); } } } if (identity != null) { Thread.CurrentPrincipal = new ClaimsPrincipal(identity); // Inside of ASP.NET this is required if (HttpContext.Current != null) { HttpContext.Current.User = Thread.CurrentPrincipal; } return(new IdentityToken(true, identity)); } return(new IdentityToken(false, null)); }
internal static async Task ValidateIdentity(ClaimsIdentity identity, ICredentialProvider credentials) { 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"); } var versionClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.VersionClaim); if (versionClaim == null) { // No version claim throw new UnauthorizedAccessException($"'{AuthenticationConstants.VersionClaim}' claim is required on skill Tokens."); } // Look for the "aud" claim, but only if issued from the Bot Framework var audienceClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value; if (string.IsNullOrWhiteSpace(audienceClaim)) { // Claim is not present or doesn't have a value. Not Authorized. throw new UnauthorizedAccessException($"'{AuthenticationConstants.AudienceClaim}' claim is required on skill Tokens."); } if (!await credentials.IsValidAppIdAsync(audienceClaim).ConfigureAwait(false)) { // The AppId is not valid. Not Authorized. throw new UnauthorizedAccessException($"Invalid audience."); } var appId = JwtTokenValidation.GetAppIdFromClaims(identity.Claims); if (string.IsNullOrWhiteSpace(appId)) { // Invalid appId throw new UnauthorizedAccessException($"Invalid appId."); } // TODO: check the appId against the registered skill client IDs. // Check the AppId and ensure that only works against my whitelist authConfig can have info on how to get the // whitelist AuthenticationConfiguration // We may need to add a ClaimsIdentityValidator delegate or class that allows the dev to inject a custom validator. }
internal static async Task ValidateIdentityAsync(ClaimsIdentity identity, ICredentialProvider credentials) { 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"); } var versionClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.VersionClaim); if (versionClaim == null) { // No version claim throw new UnauthorizedAccessException($"'{AuthenticationConstants.VersionClaim}' claim is required on skill Tokens."); } // Look for the "aud" claim, but only if issued from the Bot Framework var audienceClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value; if (string.IsNullOrWhiteSpace(audienceClaim)) { // Claim is not present or doesn't have a value. Not Authorized. throw new UnauthorizedAccessException($"'{AuthenticationConstants.AudienceClaim}' claim is required on skill Tokens."); } if (!await credentials.IsValidAppIdAsync(audienceClaim).ConfigureAwait(false)) { // The AppId is not valid. Not Authorized. throw new UnauthorizedAccessException("Invalid audience."); } var appId = JwtTokenValidation.GetAppIdFromClaims(identity.Claims); if (string.IsNullOrWhiteSpace(appId)) { // Invalid appId throw new UnauthorizedAccessException("Invalid appId."); } }
/// <summary> /// Validates a <see cref="ClaimsIdentity"/> object against the credentials and service URL provided. /// </summary> /// <param name="identity">The identity to validate.</param> /// <param name="credentials">The credentials to use for validation.</param> /// <param name="serviceUrl">The service URL to validate.</param> /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods (can't change this without breaking binary compat) public static async Task ValidateIdentity(ClaimsIdentity identity, ICredentialProvider credentials, string serviceUrl) #pragma warning restore VSTHRD200 // Use "Async" suffix for async methods { if (identity == null) { // No valid identity. Not Authorized. throw new UnauthorizedAccessException(); } if (!identity.IsAuthenticated) { // The token is in some way invalid. Not Authorized. throw new UnauthorizedAccessException(); } // 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. // Look for the "aud" claim, but only if issued from the Bot Framework Claim audienceClaim = identity.Claims.FirstOrDefault( c => c.Issuer == AuthenticationConstants.ToBotFromChannelTokenIssuer && c.Type == AuthenticationConstants.AudienceClaim); if (audienceClaim == null) { // The relevant audience Claim MUST be present. Not Authorized. throw new UnauthorizedAccessException(); } // The AppId from the claim in the token must match the AppId specified by the developer. // In this case, the token is destined for the app, so we find the app ID in the audience claim. string appIdFromClaim = audienceClaim.Value; if (string.IsNullOrWhiteSpace(appIdFromClaim)) { // Claim is present, but doesn't have a value. Not Authorized. throw new UnauthorizedAccessException(); } if (!await credentials.IsValidAppIdAsync(appIdFromClaim).ConfigureAwait(false)) { // The AppId is not valid. Not Authorized. throw new UnauthorizedAccessException($"Invalid AppId passed on token: {appIdFromClaim}"); } if (serviceUrl != null) { var serviceUrlClaim = identity.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.ServiceUrlClaim)?.Value; if (string.IsNullOrWhiteSpace(serviceUrlClaim)) { // Claim must be present. Not Authorized. throw new UnauthorizedAccessException(); } if (!string.Equals(serviceUrlClaim, serviceUrl, StringComparison.OrdinalIgnoreCase)) { // Claim must match. Not Authorized. throw new UnauthorizedAccessException(); } } }
/// <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); }
/// <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> /// <returns> /// A valid ClaimsIdentity. /// </returns> /// <remarks> /// A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. /// </remarks> public static async Task <ClaimsIdentity> AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials) { var tokenExtractor = new JwtTokenExtractor( ToBotFromEmulatorTokenValidationParameters, AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms, null); var identity = await tokenExtractor.GetIdentityAsync(authHeader); 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 == 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 == 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 if (tokenVersion == "3.0") { // The v3.0 Token types have been disallowed. Not Authorized. throw new UnauthorizedAccessException("Emulator token version '3.0' is depricated."); } else if (tokenVersion == "3.1" || tokenVersion == "3.2") { // The emulator for token versions "3.1" & "3.2" puts the AppId in the "Audiance" claim. Claim audianceClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim); if (audianceClaim == null) { // No claim around AppID. Not Authorized. throw new UnauthorizedAccessException("'aud' claim is required on Emulator Token version '3.x'."); } appID = audianceClaim.Value; } else { // Unknown Version. Not Authorized. throw new UnauthorizedAccessException($"Unknown Emulator Token version '{tokenVersion}'."); } if (!await credentials.IsValidAppIdAsync(appID)) { throw new UnauthorizedAccessException($"Invalid AppId passed on token: {appID}"); } return(identity); }