-
-
Notifications
You must be signed in to change notification settings - Fork 247
/
AzureADClient.cs
204 lines (180 loc) · 9.08 KB
/
AzureADClient.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using DSInternals.Common.Data;
using DSInternals.Common.Exceptions;
using Newtonsoft.Json;
namespace DSInternals.Common.AzureAD
{
public class AzureADClient : IDisposable
{
private const string DefaultTenantId = "myorganization";
private const string SelectParameter = "$select=userPrincipalName,searchableDeviceKey,objectId,displayName,accountEnabled";
private const string ApiVersionParameter = "api-version=1.6-internal";
private const string BatchSizeParameterFormat = "$top={0}";
private const string UPNFilterParameterFormat = "$filter=userPrincipalName eq '{0}'";
private const string IdFilterParameterFormat = "$filter=objectId eq '{0}'";
private const string AuthenticationScheme = "Bearer";
private const char UriParameterSeparator = '&';
private const string UsersUrlFormat = "https://graph.windows.net/{0}/users/{1}?";
private const string JsonContentType = "application/json";
private const string KeyCredentialAttributeName = "searchableDeviceKey";
public const int MaxBatchSize = 999;
private static readonly MediaTypeWithQualityHeaderValue s_odataContentType = MediaTypeWithQualityHeaderValue.Parse("application/json;odata=nometadata;streaming=false");
private string _tenantId;
private HttpClient _httpClient;
private readonly string _batchSizeParameter;
private JsonSerializer _jsonSerializer = JsonSerializer.CreateDefault();
public AzureADClient(string accessToken, Guid? tenantId = null, int batchSize = MaxBatchSize)
{
// Validate inputs
Validator.AssertNotNullOrWhiteSpace(accessToken, nameof(accessToken));
_tenantId = tenantId?.ToString() ?? DefaultTenantId;
_batchSizeParameter = string.Format(CultureInfo.InvariantCulture, BatchSizeParameterFormat, batchSize);
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthenticationScheme, accessToken);
_httpClient.DefaultRequestHeaders.Accept.Add(s_odataContentType);
}
public async Task<AzureADUser> GetUserAsync(string userPrincipalName)
{
// Vaidate the input
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
var filter = string.Format(CultureInfo.InvariantCulture, UPNFilterParameterFormat, userPrincipalName);
return await GetUserAsync(filter, userPrincipalName).ConfigureAwait(false);
}
public async Task<AzureADUser> GetUserAsync(Guid objectId)
{
var filter = string.Format(CultureInfo.InvariantCulture, IdFilterParameterFormat, objectId);
return await GetUserAsync(filter, objectId).ConfigureAwait(false);
}
private async Task<AzureADUser> GetUserAsync(string filterParameter, object userIdentifier)
{
// Build uri with filter
var url = new StringBuilder();
url.AppendFormat(CultureInfo.InvariantCulture, UsersUrlFormat, _tenantId, String.Empty);
url.Append(SelectParameter);
url.Append(UriParameterSeparator);
url.Append(filterParameter);
// Send the request
var result = await GetUsersAsync(url.ToString()).ConfigureAwait(false);
if ((result.Items?.Count ?? 0) == 0)
{
throw new DirectoryObjectNotFoundException(userIdentifier);
}
return result.Items[0];
}
public async Task<OdataPagedResponse<AzureADUser>> GetUsersAsync(string nextLink = null)
{
var url = new StringBuilder(nextLink);
if (string.IsNullOrEmpty(nextLink))
{
// Build the intial URL
url.AppendFormat(CultureInfo.InvariantCulture, UsersUrlFormat, _tenantId, String.Empty);
url.Append(SelectParameter);
}
// Add query string parameters
url.Append(UriParameterSeparator);
url.Append(ApiVersionParameter);
url.Append(UriParameterSeparator);
url.Append(_batchSizeParameter);
using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
{
// Perform API call
var result = await SendODataRequest<OdataPagedResponse<AzureADUser>>(request).ConfigureAwait(false);
// Update key credential owner references
if (result.Items != null)
{
result.Items.ForEach(user => user.UpdateKeyCredentialReferences());
}
return result;
}
}
public async Task SetUserAsync(string userPrincipalName, KeyCredential[] keyCredentials)
{
// Vaidate the input
Validator.AssertNotNullOrEmpty(userPrincipalName, nameof(userPrincipalName));
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
await SetUserAsync(userPrincipalName, properties).ConfigureAwait(false);
}
public async Task SetUserAsync(Guid objectId, KeyCredential[] keyCredentials)
{
var properties = new Hashtable() { { KeyCredentialAttributeName, keyCredentials } };
await SetUserAsync(objectId.ToString(), properties).ConfigureAwait(false);
}
private async Task SetUserAsync(string userIdentifier, Hashtable properties)
{
// Build the request uri
var url = new StringBuilder();
url.AppendFormat(CultureInfo.InvariantCulture, UsersUrlFormat, _tenantId, userIdentifier);
url.Append(ApiVersionParameter);
// TODO: Switch to HttpMethod.Patch after migrating to .NET Standard 2.1 / .NET 5
using (var request = new HttpRequestMessage(new HttpMethod("PATCH"), url.ToString()))
{
request.Content = new StringContent(JsonConvert.SerializeObject(properties), Encoding.UTF8, JsonContentType);
await SendODataRequest<object>(request).ConfigureAwait(false);
}
}
private async Task<T> SendODataRequest<T>(HttpRequestMessage request)
{
try
{
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if(response.StatusCode == HttpStatusCode.NoContent)
{
// No objects have been returned, but the call was successful.
return default(T);
}
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var streamReader = new StreamReader(responseStream))
{
if (s_odataContentType.MediaType.Equals(response.Content.Headers.ContentType.MediaType, StringComparison.InvariantCultureIgnoreCase))
{
// The response is a JSON document
using (var jsonTextReader = new JsonTextReader(streamReader))
{
if (response.StatusCode == HttpStatusCode.OK)
{
return _jsonSerializer.Deserialize<T>(jsonTextReader);
}
else
{
// Translate OData response to an exception
var error = _jsonSerializer.Deserialize<OdataErrorResponse>(jsonTextReader);
throw error.GetException();
}
}
}
else
{
// The response is not a JSON document, so we parse its first line as message text
string message = await streamReader.ReadLineAsync().ConfigureAwait(false);
throw new GraphApiException(message, response.StatusCode.ToString());
}
}
}
}
catch (JsonException e)
{
throw new GraphApiException("The data returned by the REST API call has an unexpected format.", e);
}
catch (HttpRequestException e)
{
// Unpack a more meaningful message, e. g. DNS error
throw new GraphApiException(e?.InnerException.Message ?? "An error occured while trying to call the REST API.", e);
}
}
#region IDisposable Support
public virtual void Dispose()
{
_httpClient.Dispose();
}
#endregion
}
}