MicrosoftAppCredentials.cs
1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Net.Http;
5 using System.Net.Http.Headers;
6 using System.Threading;
7 using System.Threading.Tasks;
8 
9 #if !NET45
10 using Microsoft.Extensions.Configuration;
11 using Microsoft.Extensions.Logging;
12 #endif
13 using Microsoft.Rest;
14 using Newtonsoft.Json;
15 
16 #if NET45
17 using System.Configuration;
18 using System.Diagnostics;
19 using System.Runtime.Serialization;
20 #endif
21 
22 namespace Microsoft.Bot.Connector
23 {
24  public class MicrosoftAppCredentials : ServiceClientCredentials
25  {
29  public const string MicrosoftAppIdKey = "MicrosoftAppId";
30 
34  public const string MicrosoftAppPasswordKey = "MicrosoftAppPassword";
35 
36  protected static ConcurrentDictionary<string, DateTime> TrustedHostNames = new ConcurrentDictionary<string, DateTime>(
37  new Dictionary<string, DateTime>() {
38  { "state.botframework.com", DateTime.MaxValue }
39  });
40 
41  protected static readonly ConcurrentDictionary<string, OAuthResponse> cache = new ConcurrentDictionary<string, OAuthResponse>();
42 
43 #if !NET45
44  protected ILogger logger;
45 #endif
46 
47 #if NET45
48  public MicrosoftAppCredentials(string appId = null, string password = null)
49  {
50  MicrosoftAppId = appId;
51  MicrosoftAppPassword = password;
52 
53  if(appId == null)
54  {
55  MicrosoftAppId = ConfigurationManager.AppSettings[MicrosoftAppIdKey] ?? Environment.GetEnvironmentVariable(MicrosoftAppIdKey, EnvironmentVariableTarget.Process);
56  }
57 
58  if(password == null)
59  {
60  MicrosoftAppPassword = ConfigurationManager.AppSettings[MicrosoftAppPasswordKey] ?? Environment.GetEnvironmentVariable(MicrosoftAppPasswordKey, EnvironmentVariableTarget.Process);
61  }
62 
63  TokenCacheKey = $"{MicrosoftAppId}-cache";
64  }
65 #else
66  public MicrosoftAppCredentials(string appId = null, string password = null, ILogger logger = null)
67  {
68  MicrosoftAppId = appId;
69  MicrosoftAppPassword = password;
70 
71  TokenCacheKey = $"{MicrosoftAppId}-cache";
72  this.logger = logger;
73  }
74 
75  public MicrosoftAppCredentials(IConfiguration configuration, ILogger logger = null)
76  : this(configuration.GetSection(MicrosoftAppIdKey)?.Value, configuration.GetSection(MicrosoftAppPasswordKey)?.Value, logger)
77  {
78  }
79 #endif
80 
81 
82 
83  public string MicrosoftAppId { get; set; }
84  public string MicrosoftAppPassword { get; set; }
85 
86  public virtual string OAuthEndpoint { get { return JwtConfig.ToChannelFromBotLoginUrl; } }
87  public virtual string OAuthScope { get { return JwtConfig.ToChannelFromBotOAuthScope; } }
88 
89  protected readonly string TokenCacheKey;
90 
97  public static void TrustServiceUrl(string serviceUrl, DateTime expirationTime = default(DateTime))
98  {
99  try
100  {
101  if (expirationTime == default(DateTime))
102  {
103  // by default the service url is valid for one day
104  TrustedHostNames.AddOrUpdate(new Uri(serviceUrl).Host, DateTime.UtcNow.AddDays(1), (key, oldValue) => DateTime.UtcNow.AddDays(1));
105  }
106  else
107  {
108  TrustedHostNames.AddOrUpdate(new Uri(serviceUrl).Host, expirationTime, (key, oldValue) => expirationTime);
109  }
110  }
111  catch (Exception)
112  {
113 #if NET45
114  Trace.TraceWarning($"Service url {serviceUrl} is not a well formed Uri!");
115 #endif
116  }
117  }
118 
124  public static bool IsTrustedServiceUrl(string serviceUrl)
125  {
126  Uri uri;
127  if (Uri.TryCreate(serviceUrl, UriKind.Absolute, out uri))
128  {
129  return TrustedUri(uri);
130  }
131  return false;
132  }
133 
138  public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
139  {
140  if (ShouldSetToken(request))
141  {
142  request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await GetTokenAsync());
143  }
144  await base.ProcessHttpRequestAsync(request, cancellationToken);
145  }
146 
147 
148 
149  public async Task<string> GetTokenAsync(bool forceRefresh = false)
150  {
151  string token;
152  OAuthResponse oAuthToken;
153  if (cache.TryGetValue(TokenCacheKey, out oAuthToken) && !forceRefresh && TokenNotExpired(oAuthToken))
154  {
155  token = oAuthToken.access_token;
156  }
157  else
158  {
159  oAuthToken = await RefreshTokenAsync().ConfigureAwait(false);
160  cache.AddOrUpdate(TokenCacheKey, oAuthToken, (key, oldToken) => oAuthToken);
161  token = oAuthToken.access_token;
162  }
163  return token;
164  }
165 
166  private bool ShouldSetToken(HttpRequestMessage request)
167  {
168  if (TrustedUri(request.RequestUri))
169  {
170  return true;
171  }
172 
173 #if NET45
174  Trace.TraceWarning($"Service url {request.RequestUri.Authority} is not trusted and JwtToken cannot be sent to it.");
175 #else
176  logger?.LogWarning($"Service url {request.RequestUri.Authority} is not trusted and JwtToken cannot be sent to it.");
177 #endif
178  return false;
179  }
180 
181  private static bool TrustedUri(Uri uri)
182  {
183  DateTime trustedServiceUrlExpiration;
184  if (TrustedHostNames.TryGetValue(uri.Host, out trustedServiceUrlExpiration))
185  {
186  // check if the trusted service url is still valid
187  if (trustedServiceUrlExpiration > DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(5)))
188  {
189  return true;
190  }
191  }
192  return false;
193  }
194 
195 #if NET45
196  [Serializable]
197 #endif
198  public sealed class OAuthException : Exception
199  {
200  public OAuthException(string body, Exception inner)
201  : base(body, inner)
202  {
203  }
204 
205 #if NET45
206  private OAuthException(SerializationInfo info, StreamingContext context)
207  : base(info, context)
208  {
209  }
210 #endif
211  }
212 
213  private async Task<OAuthResponse> RefreshTokenAsync()
214  {
215  using (HttpClient httpClient = new HttpClient())
216  {
217  var content = new FormUrlEncodedContent(new Dictionary<string, string>()
218  {
219  { "grant_type", "client_credentials" },
220  { "client_id", MicrosoftAppId },
221  { "client_secret", MicrosoftAppPassword },
222  { "scope", OAuthScope }
223  });
224 
225  using (var response = await httpClient.PostAsync(OAuthEndpoint, content).ConfigureAwait(false))
226  {
227  string body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
228 
229  try
230  {
231  response.EnsureSuccessStatusCode();
232 
233  var oauthResponse = JsonConvert.DeserializeObject<OAuthResponse>(body);
234  oauthResponse.expiration_time = DateTime.UtcNow.AddSeconds(oauthResponse.expires_in).Subtract(TimeSpan.FromSeconds(60));
235  return oauthResponse;
236  }
237  catch (Exception error)
238  {
239  throw new OAuthException(body, error);
240  }
241  }
242  }
243  }
244 
245  private bool TokenNotExpired(OAuthResponse token)
246  {
247  return token.expiration_time > DateTime.UtcNow;
248  }
249 
250  protected class OAuthResponse
251  {
252  public string token_type { get; set; }
253  public int expires_in { get; set; }
254  public string access_token { get; set; }
255  public DateTime expiration_time { get; set; }
256  }
257  }
258 }
static void TrustServiceUrl(string serviceUrl, DateTime expirationTime=default(DateTime))
Adds the host of service url to MicrosoftAppCredentials trusted hosts.
MicrosoftAppCredentials(string appId=null, string password=null, ILogger logger=null)
Configuration for JWT tokens
Definition: JwtConfig.cs:13
override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Apply the credentials to the HTTP request.
const string ToChannelFromBotOAuthScope
TO CHANNEL FROM BOT: OAuth scope to request
Definition: JwtConfig.cs:23
async Task< string > GetTokenAsync(bool forceRefresh=false)
const string ToChannelFromBotLoginUrl
TO CHANNEL FROM BOT: Login URL
Definition: JwtConfig.cs:18
MicrosoftAppCredentials(IConfiguration configuration, ILogger logger=null)
static bool IsTrustedServiceUrl(string serviceUrl)
Checks if the service url is for a trusted host or not.