Skip to content

Commit ddb2d97

Browse files
committed
Add -AccessToken parameter to Connect-Graph.
1 parent d497b45 commit ddb2d97

File tree

8 files changed

+177
-78
lines changed

8 files changed

+177
-78
lines changed

src/Authentication/Authentication/Cmdlets/ConnectGraph.cs

Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets
66
using Microsoft.Graph.Auth;
77
using Microsoft.Graph.PowerShell.Authentication.Helpers;
88
using Microsoft.Graph.PowerShell.Authentication.Models;
9-
using Microsoft.Graph.PowerShell.Authentication.Extensions;
109
using Microsoft.Identity.Client;
1110
using System;
1211
using System.Collections.Generic;
@@ -15,30 +14,50 @@ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets
1514
using System.Net.Http;
1615
using System.Threading;
1716
using System.Threading.Tasks;
17+
using System.Net;
18+
using System.Globalization;
1819

1920
[Cmdlet(VerbsCommunications.Connect, "Graph", DefaultParameterSetName = Constants.UserParameterSet)]
2021
public class ConnectGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssemblyCleanup
2122
{
22-
[Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1, HelpMessage = "An array of delegated permissions to consent to.")]
23+
[Parameter(ParameterSetName = Constants.UserParameterSet,
24+
Position = 1,
25+
HelpMessage = "An array of delegated permissions to consent to.")]
2326
public string[] Scopes { get; set; }
2427

25-
[Parameter(ParameterSetName = Constants.AppParameterSet, Position = 1, Mandatory = true, HelpMessage = "The client id of your application.")]
28+
[Parameter(ParameterSetName = Constants.AppParameterSet,
29+
Position = 1,
30+
Mandatory = true,
31+
HelpMessage = "The client id of your application.")]
2632
public string ClientId { get; set; }
2733

28-
[Parameter(ParameterSetName = Constants.AppParameterSet, Position = 2, HelpMessage = "The name of your certificate. The Certificate will be retrieved from the current user's certificate store.")]
34+
[Parameter(ParameterSetName = Constants.AppParameterSet,
35+
Position = 2,
36+
HelpMessage = "The name of your certificate. The Certificate will be retrieved from the current user's certificate store.")]
2937
public string CertificateName { get; set; }
3038

31-
[Parameter(ParameterSetName = Constants.AppParameterSet, Position = 3, HelpMessage = "The thumbprint of your certificate. The Certificate will be retrieved from the current user's certificate store.")]
39+
[Parameter(ParameterSetName = Constants.AppParameterSet,
40+
Position = 3,
41+
HelpMessage = "The thumbprint of your certificate. The Certificate will be retrieved from the current user's certificate store.")]
3242
public string CertificateThumbprint { get; set; }
33-
34-
[Parameter(Position = 4, HelpMessage = "The id of the tenant to connect to.")]
43+
44+
[Parameter(ParameterSetName = Constants.AccessTokenParameterSet,
45+
Position = 1,
46+
HelpMessage = "Specifies a bearer token for Microsoft Graph service. Access tokens do timeout and you'll have to handle their refresh.")]
47+
public string AccessToken { get; set; }
48+
49+
[Parameter(Position = 4,
50+
HelpMessage = "The id of the tenant to connect to.")]
3551
public string TenantId { get; set; }
3652

37-
[Parameter(Position = 5, HelpMessage = "Forces the command to get a new access token silently.")]
53+
[Parameter(Position = 5,
54+
HelpMessage = "Forces the command to get a new access token silently.")]
3855
public SwitchParameter ForceRefresh { get; set; }
3956

40-
[Parameter(Mandatory = false, HelpMessage = "Determines the scope of authentication context. This accepts `Process` for the current process, or `CurrentUser` for all sessions started by user.")]
57+
[Parameter(Mandatory = false,
58+
HelpMessage = "Determines the scope of authentication context. This accepts `Process` for the current process, or `CurrentUser` for all sessions started by user.")]
4159
public ContextScope ContextScope { get; set; }
60+
4261
private CancellationTokenSource cancellationTokenSource;
4362

4463
protected override void BeginProcessing()
@@ -55,41 +74,57 @@ protected override void EndProcessing()
5574
protected override void ProcessRecord()
5675
{
5776
base.ProcessRecord();
77+
IAuthContext authContext = new AuthContext { TenantId = TenantId };
78+
cancellationTokenSource = new CancellationTokenSource();
5879

59-
IAuthContext authConfig = new AuthContext { TenantId = TenantId };
60-
61-
if (ParameterSetName == Constants.UserParameterSet)
80+
switch (ParameterSetName)
6281
{
63-
// 2 mins timeout. 1 min < HTTP timeout.
64-
TimeSpan authTimeout = new TimeSpan(0, 0, Constants.MaxDeviceCodeTimeOut);
65-
cancellationTokenSource = new CancellationTokenSource(authTimeout);
66-
authConfig.AuthType = AuthenticationType.Delegated;
67-
authConfig.Scopes = Scopes ?? new string[] { "User.Read" };
68-
// Default to CurrentUser but allow the customer to change this via `ContextScope` param.
69-
authConfig.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.CurrentUser;
70-
}
71-
else
72-
{
73-
cancellationTokenSource = new CancellationTokenSource();
74-
authConfig.AuthType = AuthenticationType.AppOnly;
75-
authConfig.ClientId = ClientId;
76-
authConfig.CertificateThumbprint = CertificateThumbprint;
77-
authConfig.CertificateName = CertificateName;
78-
// Default to Process but allow the customer to change this via `ContextScope` param.
79-
authConfig.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.Process;
82+
case Constants.UserParameterSet:
83+
{
84+
// 2 mins timeout. 1 min < HTTP timeout.
85+
TimeSpan authTimeout = new TimeSpan(0, 0, Constants.MaxDeviceCodeTimeOut);
86+
cancellationTokenSource = new CancellationTokenSource(authTimeout);
87+
authContext.AuthType = AuthenticationType.Delegated;
88+
authContext.Scopes = Scopes ?? new string[] { "User.Read" };
89+
// Default to CurrentUser but allow the customer to change this via `ContextScope` param.
90+
authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.CurrentUser;
91+
}
92+
break;
93+
case Constants.AppParameterSet:
94+
{
95+
authContext.AuthType = AuthenticationType.AppOnly;
96+
authContext.ClientId = ClientId;
97+
authContext.CertificateThumbprint = CertificateThumbprint;
98+
authContext.CertificateName = CertificateName;
99+
// Default to Process but allow the customer to change this via `ContextScope` param.
100+
authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.Process;
101+
}
102+
break;
103+
case Constants.AccessTokenParameterSet:
104+
{
105+
authContext.AuthType = AuthenticationType.UserProvidedAccessToken;
106+
authContext.ContextScope = ContextScope.Process;
107+
// Store user provided access token to a session object.
108+
GraphSession.Instance.UserProvidedToken = new NetworkCredential(string.Empty, AccessToken).SecurePassword;
109+
}
110+
break;
80111
}
81112

82113
CancellationToken cancellationToken = cancellationTokenSource.Token;
83114

84115
try
85116
{
86117
// Gets a static instance of IAuthenticationProvider when the client app hasn't changed.
87-
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authConfig);
118+
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authContext);
88119
IClientApplicationBase clientApplication = null;
89120
if (ParameterSetName == Constants.UserParameterSet)
121+
{
90122
clientApplication = (authProvider as DeviceCodeProvider).ClientApplication;
91-
else
123+
}
124+
else if (ParameterSetName == Constants.AppParameterSet)
125+
{
92126
clientApplication = (authProvider as ClientCredentialProvider).ClientApplication;
127+
}
93128

94129
// Incremental scope consent without re-instantiating the auth provider. We will use a static instance.
95130
GraphRequestContext graphRequestContext = new GraphRequestContext();
@@ -102,7 +137,7 @@ protected override void ProcessRecord()
102137
{
103138
AuthenticationProviderOption = new AuthenticationProviderOption
104139
{
105-
Scopes = authConfig.Scopes,
140+
Scopes = authContext.Scopes,
106141
ForceRefresh = ForceRefresh
107142
}
108143
}
@@ -114,24 +149,32 @@ protected override void ProcessRecord()
114149
httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), graphRequestContext);
115150
authProvider.AuthenticateRequestAsync(httpRequestMessage).GetAwaiter().GetResult();
116151

117-
var accounts = clientApplication.GetAccountsAsync().GetAwaiter().GetResult();
118-
var account = accounts.FirstOrDefault();
119-
120-
JwtPayload jwtPayload = JwtHelpers.DecodeToObject<JwtPayload>(httpRequestMessage.Headers.Authorization?.Parameter);
121-
authConfig.Scopes = jwtPayload?.Scp?.Split(' ') ?? jwtPayload?.Roles;
122-
authConfig.TenantId = jwtPayload?.Tid ?? account?.HomeAccountId?.TenantId;
123-
authConfig.AppName = jwtPayload?.AppDisplayname;
124-
authConfig.Account = jwtPayload?.Upn ?? account?.Username;
152+
IAccount account = null;
153+
if (clientApplication != null)
154+
{
155+
// Only get accounts when we are using MSAL to get an access token.
156+
IEnumerable<IAccount> accounts = clientApplication.GetAccountsAsync().GetAwaiter().GetResult();
157+
account = accounts.FirstOrDefault();
158+
}
159+
DecodeJWT(httpRequestMessage.Headers.Authorization?.Parameter, account, ref authContext);
125160

126161
// Save auth context to session state.
127-
GraphSession.Instance.AuthContext = authConfig;
162+
GraphSession.Instance.AuthContext = authContext;
128163
}
129164
catch (AuthenticationException authEx)
130165
{
131166
if ((authEx.InnerException is TaskCanceledException) && cancellationToken.IsCancellationRequested)
132-
throw new Exception($"Device code terminal timed-out after {Constants.MaxDeviceCodeTimeOut} seconds. Please try again.");
167+
{
168+
// DeviceCodeTimeout
169+
throw new Exception(string.Format(
170+
CultureInfo.CurrentCulture,
171+
ErrorConstants.Message.DeviceCodeTimeout,
172+
Constants.MaxDeviceCodeTimeOut));
173+
}
133174
else
175+
{
134176
throw authEx.InnerException ?? authEx;
177+
}
135178
}
136179
catch (Exception ex)
137180
{
@@ -173,6 +216,24 @@ private void ThrowParameterError(string parameterName)
173216
);
174217
}
175218

219+
private void DecodeJWT(string token, IAccount account, ref IAuthContext authContext)
220+
{
221+
JwtPayload jwtPayload = JwtHelpers.DecodeToObject<JwtPayload>(token);
222+
if (jwtPayload == null && authContext.AuthType == AuthenticationType.UserProvidedAccessToken)
223+
{
224+
throw new Exception(string.Format(
225+
CultureInfo.CurrentCulture,
226+
ErrorConstants.Message.InvalidUserProvidedToken,
227+
nameof(AccessToken)));
228+
}
229+
230+
authContext.ClientId = jwtPayload?.Appid ?? authContext.ClientId;
231+
authContext.Scopes = jwtPayload?.Scp?.Split(' ') ?? jwtPayload?.Roles;
232+
authContext.TenantId = jwtPayload?.Tid ?? account?.HomeAccountId?.TenantId;
233+
authContext.AppName = jwtPayload?.AppDisplayname;
234+
authContext.Account = jwtPayload?.Upn ?? account?.Username;
235+
}
236+
176237
/// <summary>
177238
/// Globally initializes GraphSession.
178239
/// </summary>

src/Authentication/Authentication/Common/GraphSession.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Microsoft.Graph.PowerShell.Authentication
66
{
77
using System;
8+
using System.Security;
89
using System.Threading;
910
/// <summary>
1011
/// Contains methods to create, modify or obtain a thread safe static instance of <see cref="GraphSession"/>.
@@ -24,18 +25,22 @@ public class GraphSession : IGraphSession
2425
/// </summary>
2526
public IAuthContext AuthContext { get; set; }
2627

27-
private byte[] token;
28+
private byte[] msalToken;
2829

2930
/// <summary>
3031
/// Gets or Sets a session based token.
3132
/// This returns an empty byte[] when token is not present.
3233
/// </summary>
33-
public byte[] Token
34+
public byte[] MSALToken
3435
{
35-
get { return token ?? new byte[0]; }
36-
set { token = value; }
36+
get { return msalToken ?? new byte[0]; }
37+
set { msalToken = value; }
3738
}
3839

40+
/// <summary>
41+
/// Gets or Sets a user provided access token for calling Microsoft Graph service.
42+
/// </summary>
43+
public SecureString UserProvidedToken { get; set; }
3944

4045
/// <summary>
4146
/// The name of the selected Microsoft Graph profile.

src/Authentication/Authentication/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public static class Constants
1212
public const string SDKHeaderValue = "Graph-powershell-{0}-{1}.{2}.{3}";
1313
internal const string UserParameterSet = "UserParameterSet";
1414
internal const string AppParameterSet = "AppParameterSet";
15+
internal const string AccessTokenParameterSet = "AccessTokenParameterSet";
1516
internal const int MaxDeviceCodeTimeOut = 120; // 2 mins timeout.
1617
internal static readonly string TokenCacheDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".graph");
1718
internal const string ProfileDescription = "A snapshot of the Microsoft Graph {0} API for {1} cloud.";

src/Authentication/Authentication/ErrorConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ internal static class Message
2121
internal const string MissingAuthContext = "Authentication needed, call Connect-Graph.";
2222
internal const string NullOrEmptyParameter = "Parameter '{0}' cannot be null or empty.";
2323
internal const string MacKeyChainFailed = "{0} failed with result code {1}.";
24+
internal const string DeviceCodeTimeout = "Device code terminal timed-out after {0} seconds. Please try again.";
25+
internal const string InvalidUserProvidedToken = "The provided access token is invalid. Set a valid access token to `-{0}` parameter and try again.";
2426
}
2527
}
2628
}

0 commit comments

Comments
 (0)