-
Notifications
You must be signed in to change notification settings - Fork 361
/
Copy pathTokenHandling.cs
212 lines (196 loc) · 12.1 KB
/
TokenHandling.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
205
206
207
208
209
210
211
212
using Microsoft.SharePoint.Client;
using PnP.PowerShell.Commands.Attributes;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Management.Automation;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace PnP.PowerShell.Commands.Base
{
internal static class TokenHandler
{
internal static void ValidateTokenForPermissions(Type cmdletType, string token)
{
string[] requiredScopes = null;
var requiredScopesAttribute = (RequiredMinimalApiPermissions)Attribute.GetCustomAttribute(cmdletType, typeof(RequiredMinimalApiPermissions));
if (requiredScopesAttribute != null)
{
requiredScopes = requiredScopesAttribute.PermissionScopes;
}
if (requiredScopes?.Length > 0)
{
var decodedToken = new JwtSecurityToken(token);
var roles = decodedToken.Claims.FirstOrDefault(c => c.Type == "roles");
if (roles != null)
{
foreach (var permission in requiredScopes)
{
if (!roles.Value.ToLower().Contains(permission.ToLower()))
{
throw new PSArgumentException($"Authorization Denied: Token used does not contain permission scope '{permission}'");
}
}
}
roles = decodedToken.Claims.FirstOrDefault(c => c.Type == "scp");
if (roles != null)
{
foreach (var permission in requiredScopes)
{
if (!roles.Value.ToLower().Contains(permission.ToLower()))
{
throw new PSArgumentException($"Authorization Denied: Token used does not contain permission scope '{permission}'");
}
}
}
}
}
internal static string GetAccessToken(Cmdlet cmdlet, string appOnlyDefaultScope, PnPConnection connection)
{
var contextSettings = connection.Context.GetContextSettings();
var authManager = contextSettings.AuthenticationManager;
if (authManager != null)
{
if (contextSettings.Type == Framework.Utilities.Context.ClientContextType.SharePointACSAppOnly)
{
// When connected using ACS, we cannot get a token for another endpoint
throw new PSInvalidOperationException("Trying to get a token for a different endpoint while being connected through an ACS token is not possible. Please connect differently.");
}
string[] requiredScopes = null;
RequiredMinimalApiPermissions requiredScopesAttribute = null;
if (cmdlet != null && cmdlet.GetType() != null)
{
requiredScopesAttribute = (RequiredMinimalApiPermissions)Attribute.GetCustomAttribute(cmdlet.GetType(), typeof(RequiredMinimalApiPermissions));
}
if (requiredScopesAttribute != null)
{
requiredScopes = requiredScopesAttribute.PermissionScopes;
}
if (contextSettings.Type == Framework.Utilities.Context.ClientContextType.AzureADCertificate)
{
requiredScopes = new[] { appOnlyDefaultScope }; // override for app only
}
if (requiredScopes == null && !string.IsNullOrEmpty(appOnlyDefaultScope))
{
requiredScopes = new[] { appOnlyDefaultScope };
}
cmdlet.WriteVerbose($"Acquiring oAuth token for {(requiredScopes.Length != 1 ? requiredScopes.Length + " " : "")}permission scope{(requiredScopes.Length != 1 ? "s" : "")} {string.Join(",", requiredScopes)}");
var accessToken = authManager.GetAccessTokenAsync(requiredScopes).GetAwaiter().GetResult();
cmdlet.WriteVerbose($"Access token acquired: {accessToken}");
return accessToken;
}
return null;
}
/// <summary>
/// Returns an access token based on a Managed Identity. Only works within Azure components supporting managed identities such as Azure Functions and Azure Runbooks.
/// </summary>
/// <param name="cmdlet">The cmdlet scope in which this code runs. Used to write logging to.</param>
/// <param name="httpClient">The HttpClient that will be reused to fetch the token to avoid port exhaustion</param>
/// <param name="defaultResource">If the cmdlet being executed does not have an attribute to indicate the required permissions, this permission will be requested instead. Optional.</param>
/// <param name="userAssignedManagedIdentityObjectId">The object/principal Id of the user assigned managed identity to be used. If userAssignedManagedIdentityObjectId. userAssignedManagedIdentityClientId and userAssignedManagedIdentityAzureResourceId are omitted, a system assigned managed identity will be used.</param>
/// <param name="userAssignedManagedIdentityClientId">The client Id of the user assigned managed identity to be used. If userAssignedManagedIdentityObjectId, userAssignedManagedIdentityClientId and userAssignedManagedIdentityAzureResourceId are omitted, a system assigned managed identity will be used.</param>
/// <param name="userAssignedManagedIdentityAzureResourceId">The Azure Resource Id of the user assigned managed identity to be used. If userAssignedManagedIdentityObjectId, userAssignedManagedIdentityClientId and userAssignedManagedIdentityAzureResourceId are omitted, a system assigned managed identity will be used.</param>
/// <returns>Access token</returns>
/// <exception cref="PSInvalidOperationException">Thrown if unable to retrieve an access token through a managed identity</exception>
internal static async Task<string> GetManagedIdentityTokenAsync(Cmdlet cmdlet, HttpClient httpClient, string defaultResource, string userAssignedManagedIdentityObjectId = null, string userAssignedManagedIdentityClientId = null, string userAssignedManagedIdentityAzureResourceId = null)
{
string requiredScope = null;
var requiredScopesAttribute = (RequiredMinimalApiPermissions)Attribute.GetCustomAttribute(cmdlet.GetType(), typeof(RequiredMinimalApiPermissions));
if (requiredScopesAttribute != null)
{
requiredScope = requiredScopesAttribute.PermissionScopes.First();
if (requiredScope.ToLower().StartsWith("https://"))
{
var uri = new Uri(requiredScope);
requiredScope = $"https://{uri.Host}/";
}
else
{
requiredScope = defaultResource;
}
cmdlet.WriteVerbose($"Using scope {requiredScope} for managed identity token coming from the cmdlet permission attribute");
}
else
{
requiredScope = defaultResource;
cmdlet.WriteVerbose($"Using scope {requiredScope} for managed identity token coming from the passed in default resource");
}
var endPoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT");
cmdlet.WriteVerbose($"Using identity endpoint: {endPoint}");
var identityHeader = Environment.GetEnvironmentVariable("IDENTITY_HEADER");
cmdlet.WriteVerbose($"Using identity header: {identityHeader}");
if (string.IsNullOrEmpty(endPoint))
{
endPoint = Environment.GetEnvironmentVariable("MSI_ENDPOINT");
identityHeader = Environment.GetEnvironmentVariable("MSI_SECRET");
}
if (string.IsNullOrEmpty(endPoint))
{
// additional fallback
// using well-known endpoint for Instance Metadata Service, useful in Azure VM scenario.
// https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
endPoint = "http://169.254.169.254/metadata/identity/oauth2/token";
}
if (!string.IsNullOrEmpty(endPoint))
{
var tokenRequestUrl = $"{endPoint}?resource={requiredScope}&api-version=2019-08-01";
// Check if we're using a user assigned managed identity
if (!string.IsNullOrEmpty(userAssignedManagedIdentityClientId))
{
// User assigned managed identity will be used, provide the client Id of the user assigned managed identity to use
cmdlet.WriteVerbose($"Using the user assigned managed identity with client ID: {userAssignedManagedIdentityClientId}");
tokenRequestUrl += $"&client_id={userAssignedManagedIdentityClientId}";
}
else if (!string.IsNullOrEmpty(userAssignedManagedIdentityObjectId))
{
// User assigned managed identity will be used, provide the object/pricipal Id of the user assigned managed identity to use
// Note 16-02-2023: principal_id is an alias of object_id, but does not work on Azure Automation at the time of writing, while object_id works on both.
cmdlet.WriteVerbose($"Using the user assigned managed identity with object/principal ID: {userAssignedManagedIdentityObjectId}");
tokenRequestUrl += $"&object_id={userAssignedManagedIdentityObjectId}";
}
else if (!string.IsNullOrEmpty(userAssignedManagedIdentityAzureResourceId))
{
// User assigned managed identity will be used, provide the Azure Resource Id of the user assigned managed identity to use
cmdlet.WriteVerbose($"Using the user assigned managed identity with Azure Resource ID: {userAssignedManagedIdentityAzureResourceId}");
tokenRequestUrl += $"&mi_res_id={userAssignedManagedIdentityAzureResourceId}";
}
else
{
cmdlet.WriteVerbose("Using the system assigned managed identity");
}
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, tokenRequestUrl))
{
requestMessage.Version = new Version(2, 0);
requestMessage.Headers.Add("Metadata", "true");
if (!string.IsNullOrEmpty(identityHeader))
{
requestMessage.Headers.Add("X-IDENTITY-HEADER", identityHeader);
}
cmdlet.WriteVerbose($"Sending token request to {tokenRequestUrl}");
var response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var responseElement = JsonSerializer.Deserialize<JsonElement>(responseContent);
if (responseElement.TryGetProperty("access_token", out JsonElement accessTokenElement))
{
var accessToken = accessTokenElement.GetString();
return accessToken;
}
}
else
{
var errorMessage = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new PSInvalidOperationException(errorMessage);
}
}
}
else
{
throw new PSInvalidOperationException("Cannot determine Managed Identity Endpoint URL to acquire token.");
}
return null;
}
}
}