public void ShouldUseCertThumbPrintInsteadOfPassedInCertificateWhenBothAreSpecified() { // Arrange var dummyCertName = "CN=dummycert"; var inMemoryCertName = "CN=inmemorycert"; var storedDummyCertificate = CreateAndStoreSelfSignedCert(dummyCertName); var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName); AuthContext appOnlyAuthContext = new AuthContext { AuthType = AuthenticationType.AppOnly, ClientId = Guid.NewGuid().ToString(), CertificateThumbprint = storedDummyCertificate.Thumbprint, Certificate = inMemoryCertificate, ContextScope = ContextScope.Process }; // Act IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext); // Assert Assert.IsType <ClientCredentialProvider>(authProvider); var clientCredentialProvider = (ClientCredentialProvider)authProvider; // Assert: That the certificate used is dummycert (Thumbprint), that is in the store Assert.NotEqual(inMemoryCertName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name); Assert.Equal(appOnlyAuthContext.CertificateThumbprint, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.Thumbprint); //CleanUp DeleteSelfSignedCertByThumbprint(appOnlyAuthContext.CertificateThumbprint); GraphSession.Reset(); }
public async Task ShouldUseDelegateAuthProviderWhenUserAccessTokenIsProvidedAsync() { // Arrange string accessToken = "ACCESS_TOKEN_VIA_DELEGATE_PROVIDER"; GraphSession.Instance.UserProvidedToken = new NetworkCredential(string.Empty, accessToken).SecurePassword; AuthContext userProvidedAuthContext = new AuthContext { AuthType = AuthenticationType.UserProvidedAccessToken, ContextScope = ContextScope.Process }; IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(userProvidedAuthContext); HttpRequestMessage requestMessage = new HttpRequestMessage(); // Act await authProvider.AuthenticateRequestAsync(requestMessage); // Assert Assert.IsType <DelegateAuthenticationProvider>(authProvider); Assert.Equal("Bearer", requestMessage.Headers.Authorization.Scheme); Assert.Equal(accessToken, requestMessage.Headers.Authorization.Parameter); // reset static instance. GraphSession.Reset(); }
public void ShouldThrowIfNullInMemoryCertIsProvided() { // Arrange AuthContext appOnlyAuthContext = new AuthContext { AuthType = AuthenticationType.AppOnly, ClientId = Guid.NewGuid().ToString(), Certificate = null, ContextScope = ContextScope.Process }; // Act Action action = () => AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext); //Assert Assert.Throws <ArgumentNullException>(action); }
public void ShouldThrowIfNonExistentCertNameIsProvided() { // Arrange var dummyCertName = "CN=NonExistingCert"; AuthContext appOnlyAuthContext = new AuthContext { AuthType = AuthenticationType.AppOnly, ClientId = Guid.NewGuid().ToString(), CertificateName = dummyCertName, ContextScope = ContextScope.Process }; // Act Action action = () => AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext); //Assert Assert.ThrowsAny <Exception>(action); }
/// <summary> /// Gets a Custom AuthProvider or configured default provided depending on Auth Scheme specified. /// </summary> /// <returns></returns> private IAuthenticationProvider GetAuthProvider() { if (Authentication == GraphRequestAuthenticationType.UserProvidedToken) { return(new InvokeGraphRequestAuthProvider(GraphRequestSession)); } // Ensure that AuthContext is present in DefaultAuth mode, otherwise demand for Connect-Graph to be called. if (Authentication == GraphRequestAuthenticationType.Default && GraphSession.Instance.AuthContext != null) { return(AuthenticationHelpers.GetAuthProvider(GraphSession.Instance.AuthContext)); } else { var error = new ArgumentNullException(Resources.MissingAuthenticationContext.FormatCurrentCulture(nameof(GraphSession.Instance.AuthContext)), nameof(GraphSession.Instance.AuthContext)); throw error; } }
public void ShouldUseInteractiveProviderWhenDelegated() { // Arrange AuthContext delegatedAuthContext = new AuthContext { AuthType = AuthenticationType.Delegated, Scopes = new[] { "User.Read" }, ContextScope = ContextScope.Process }; // Act IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(delegatedAuthContext); // Assert Assert.IsType <InteractiveAuthenticationProvider>(authProvider); // reset static instance. GraphSession.Reset(); }
public void GetGraphHttpClientWithClientTimeoutParameterShouldReturnHttpClientWithSpecifiedTimeout() { GraphSession.Initialize(() => new GraphSession()); TimeSpan timeSpan = TimeSpan.FromSeconds(10); var authContext = new AuthContext { AuthType = AuthenticationType.UserProvidedAccessToken, ContextScope = ContextScope.Process }; IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authContext); HttpClient httpClient = HttpHelpers.GetGraphHttpClient(authProvider, timeSpan); Assert.Equal(authContext.ClientTimeout, TimeSpan.FromSeconds(Constants.ClientTimeout)); Assert.Equal(httpClient.Timeout, timeSpan); // reset static instance. GraphSession.Reset(); }
public void ShouldUseDeviceCodeWhenFallback() { // Arrange AuthContext delegatedAuthContext = new AuthContext { AuthType = AuthenticationType.Delegated, Scopes = new[] { "User.Read" }, ContextScope = ContextScope.Process, AuthProviderType = AuthProviderType.DeviceCodeProviderFallBack }; // Act IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(delegatedAuthContext); // Assert Assert.IsType <DeviceCodeProvider>(authProvider); // reset static instance. GraphSession.Reset(); }
public void ShouldUseInMemoryCertificateWhenProvided() { // Arrange var certificate = CreateSelfSignedCert("cn=inmemorycert"); AuthContext appOnlyAuthContext = new AuthContext { AuthType = AuthenticationType.AppOnly, ClientId = Guid.NewGuid().ToString(), Certificate = certificate, ContextScope = ContextScope.Process }; // Act IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext); // Assert Assert.IsType <ClientCredentialProvider>(authProvider); var clientCredentialProvider = (ClientCredentialProvider)authProvider; // Assert: That the certificate created and set above is the same as used here. Assert.Equal(clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate, certificate); GraphSession.Reset(); }
public void ShouldUseClientCredentialProviderWhenAppOnlyContextIsProvided() { // Arrange AuthContext appOnlyAuthContext = new AuthContext { AuthType = AuthenticationType.AppOnly, ClientId = Guid.NewGuid().ToString(), CertificateName = "cn=dummyCert", ContextScope = ContextScope.Process }; CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateName); // Act IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext); // Assert Assert.IsType <ClientCredentialProvider>(authProvider); // reset DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateName); GraphSession.Reset(); }
protected override void ProcessRecord() { base.ProcessRecord(); IAuthContext authContext = new AuthContext { TenantId = TenantId }; cancellationTokenSource = new CancellationTokenSource(); // Set selected environment to the session object. GraphSession.Instance.Environment = environment; switch (ParameterSetName) { case Constants.UserParameterSet: { // 2 mins timeout. 1 min < HTTP timeout. TimeSpan authTimeout = new TimeSpan(0, 0, Constants.MaxDeviceCodeTimeOut); cancellationTokenSource = new CancellationTokenSource(authTimeout); authContext.AuthType = AuthenticationType.Delegated; string[] processedScopes = ProcessScopes(Scopes); authContext.Scopes = processedScopes.Length == 0 ? new string[] { "User.Read" } : processedScopes; // Default to CurrentUser but allow the customer to change this via `ContextScope` param. authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.CurrentUser; } break; case Constants.AppParameterSet: { authContext.AuthType = AuthenticationType.AppOnly; authContext.ClientId = ClientId; authContext.CertificateThumbprint = CertificateThumbprint; authContext.CertificateName = CertificateName; // Default to Process but allow the customer to change this via `ContextScope` param. authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.Process; } break; case Constants.AccessTokenParameterSet: { authContext.AuthType = AuthenticationType.UserProvidedAccessToken; authContext.ContextScope = ContextScope.Process; // Store user provided access token to a session object. GraphSession.Instance.UserProvidedToken = new NetworkCredential(string.Empty, AccessToken).SecurePassword; } break; } CancellationToken cancellationToken = cancellationTokenSource.Token; try { // Gets a static instance of IAuthenticationProvider when the client app hasn't changed. IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authContext); IClientApplicationBase clientApplication = null; if (ParameterSetName == Constants.UserParameterSet) { clientApplication = (authProvider as DeviceCodeProvider).ClientApplication; } else if (ParameterSetName == Constants.AppParameterSet) { clientApplication = (authProvider as ClientCredentialProvider).ClientApplication; } // Incremental scope consent without re-instantiating the auth provider. We will use a static instance. GraphRequestContext graphRequestContext = new GraphRequestContext(); graphRequestContext.CancellationToken = cancellationToken; graphRequestContext.MiddlewareOptions = new Dictionary <string, IMiddlewareOption> { { typeof(AuthenticationHandlerOption).ToString(), new AuthenticationHandlerOption { AuthenticationProviderOption = new AuthenticationProviderOption { Scopes = authContext.Scopes, ForceRefresh = ForceRefresh } } } }; // Trigger consent. HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), graphRequestContext); authProvider.AuthenticateRequestAsync(httpRequestMessage).GetAwaiter().GetResult(); IAccount account = null; if (clientApplication != null) { // Only get accounts when we are using MSAL to get an access token. IEnumerable <IAccount> accounts = clientApplication.GetAccountsAsync().GetAwaiter().GetResult(); account = accounts.FirstOrDefault(); } DecodeJWT(httpRequestMessage.Headers.Authorization?.Parameter, account, ref authContext); // Save auth context to session state. GraphSession.Instance.AuthContext = authContext; } catch (AuthenticationException authEx) { if ((authEx.InnerException is TaskCanceledException) && cancellationToken.IsCancellationRequested) { // DeviceCodeTimeout throw new Exception(string.Format( CultureInfo.CurrentCulture, ErrorConstants.Message.DeviceCodeTimeout, Constants.MaxDeviceCodeTimeOut)); } else { throw authEx.InnerException ?? authEx; } } catch (Exception ex) { throw ex.InnerException ?? ex; } WriteObject("Welcome To Microsoft Graph!"); }
protected override void ProcessRecord() { base.ProcessRecord(); AuthConfig authConfig = new AuthConfig { TenantId = TenantId }; CancellationToken cancellationToken = CancellationToken.None; if (ParameterSetName == Constants.UserParameterSet) { // 2 mins timeout. 1 min < HTTP timeout. TimeSpan authTimeout = new TimeSpan(0, 0, Constants.MaxDeviceCodeTimeOut); CancellationTokenSource cts = new CancellationTokenSource(authTimeout); cancellationToken = cts.Token; authConfig.AuthType = AuthenticationType.Delegated; authConfig.Scopes = Scopes ?? new string[] { "User.Read" }; } else { authConfig.AuthType = AuthenticationType.AppOnly; authConfig.ClientId = ClientId; authConfig.CertificateThumbprint = CertificateThumbprint; authConfig.CertificateName = CertificateName; } try { // Gets a static instance of IAuthenticationProvider when the client app hasn't changed. IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authConfig); IClientApplicationBase clientApplication = null; if (ParameterSetName == Constants.UserParameterSet) { clientApplication = (authProvider as DeviceCodeProvider).ClientApplication; } else { clientApplication = (authProvider as ClientCredentialProvider).ClientApplication; } // Incremental scope consent without re-instanciating the auth provider. We will use a static instance. GraphRequestContext graphRequestContext = new GraphRequestContext(); graphRequestContext.CancellationToken = cancellationToken; graphRequestContext.MiddlewareOptions = new Dictionary <string, IMiddlewareOption> { { typeof(AuthenticationHandlerOption).ToString(), new AuthenticationHandlerOption { AuthenticationProviderOption = new AuthenticationProviderOption { Scopes = authConfig.Scopes, ForceRefresh = ForceRefresh } } } }; // Trigger consent. HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), graphRequestContext); authProvider.AuthenticateRequestAsync(httpRequestMessage).GetAwaiter().GetResult(); var accounts = clientApplication.GetAccountsAsync().GetAwaiter().GetResult(); var account = accounts.FirstOrDefault(); JwtPayload jwtPayload = JwtHelpers.DecodeToObject <JwtPayload>(httpRequestMessage.Headers.Authorization?.Parameter); authConfig.Scopes = jwtPayload?.Scp?.Split(' ') ?? jwtPayload?.Roles; authConfig.TenantId = jwtPayload?.Tid ?? account?.HomeAccountId?.TenantId; authConfig.AppName = jwtPayload?.AppDisplayname; authConfig.Account = jwtPayload?.Upn ?? account?.Username; // Save auth config to session state. SessionState.PSVariable.Set(Constants.GraphAuthConfigId, authConfig); } catch (AuthenticationException authEx) { if ((authEx.InnerException is TaskCanceledException) && cancellationToken.IsCancellationRequested) { throw new Exception($"Device code terminal timed-out after {Constants.MaxDeviceCodeTimeOut} seconds. Please try again."); } else { throw authEx.InnerException ?? authEx; } } catch (Exception ex) { throw ex.InnerException ?? ex; } WriteObject("Welcome To Microsoft Graph!"); }
/// <summary> /// Authenticates the client using the provided <see cref="IAuthContext"/>. /// </summary> /// <param name="authContext">The <see cref="IAuthContext"/> to authenticate.</param> /// <param name="forceRefresh">Whether or not to force refresh a token if one exists.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="fallBackWarning">Callback to report FallBack to DeviceCode Authentication</param> /// <returns></returns> public static async Task <IAuthContext> AuthenticateAsync(IAuthContext authContext, bool forceRefresh, CancellationToken cancellationToken, Action fallBackWarning = null) { // Gets a static instance of IAuthenticationProvider when the client app hasn't changed. var authProvider = AuthenticationHelpers.GetAuthProvider(authContext); IClientApplicationBase clientApplication = null; switch (authContext.AuthProviderType) { case AuthProviderType.DeviceCodeProvider: case AuthProviderType.DeviceCodeProviderFallBack: clientApplication = (authProvider as DeviceCodeProvider).ClientApplication; break; case AuthProviderType.InteractiveAuthenticationProvider: { var interactiveProvider = (authProvider as InteractiveAuthenticationProvider).ClientApplication; //When User is not Interactive, Pre-Emptively Fallback and warn, to DeviceCode if (!interactiveProvider.IsUserInteractive()) { authContext.AuthProviderType = AuthProviderType.DeviceCodeProviderFallBack; fallBackWarning?.Invoke(); var fallBackAuthContext = await AuthenticateAsync(authContext, forceRefresh, cancellationToken, fallBackWarning); return(fallBackAuthContext); } break; } case AuthProviderType.ClientCredentialProvider: { clientApplication = (authProvider as ClientCredentialProvider).ClientApplication; break; } } try { // Incremental scope consent without re-instantiating the auth provider. We will use provided instance. GraphRequestContext graphRequestContext = new GraphRequestContext(); graphRequestContext.CancellationToken = cancellationToken; graphRequestContext.MiddlewareOptions = new Dictionary <string, IMiddlewareOption> { { typeof(AuthenticationHandlerOption).ToString(), new AuthenticationHandlerOption { AuthenticationProviderOption = new AuthenticationProviderOption { Scopes = authContext.Scopes, ForceRefresh = forceRefresh } } } }; // Trigger consent. HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), graphRequestContext); await authProvider.AuthenticateRequestAsync(httpRequestMessage); IAccount account = null; if (clientApplication != null) { // Only get accounts when we are using MSAL to get an access token. IEnumerable <IAccount> accounts = clientApplication.GetAccountsAsync().GetAwaiter().GetResult(); account = accounts.FirstOrDefault(); } JwtHelpers.DecodeJWT(httpRequestMessage.Headers.Authorization?.Parameter, account, ref authContext); return(authContext); } catch (AuthenticationException authEx) { //Interactive Authentication Failure: Could Not Open Browser, fallback to DeviceAuth if (IsUnableToOpenWebPageError(authEx)) { authContext.AuthProviderType = AuthProviderType.DeviceCodeProviderFallBack; //ReAuthenticate using DeviceCode as fallback. var fallBackAuthContext = await AuthenticateAsync(authContext, forceRefresh, cancellationToken); //Indicate that this was a Fallback if (fallBackWarning != null && fallBackAuthContext.AuthProviderType == AuthProviderType.DeviceCodeProviderFallBack) { fallBackWarning(); } return(fallBackAuthContext); } if (authEx.InnerException is TaskCanceledException && cancellationToken.IsCancellationRequested) { // Authentication requets timeout. throw new Exception(string.Format(CultureInfo.CurrentCulture, ErrorConstants.Message.DeviceCodeTimeout, Constants.MaxDeviceCodeTimeOut)); } else if (authEx.InnerException is MsalServiceException msalServiceEx && msalServiceEx.StatusCode == 400 && msalServiceEx.ErrorCode == "invalid_scope" && string.IsNullOrWhiteSpace(authContext.TenantId) && (authContext.AuthProviderType == AuthProviderType.DeviceCodeProvider || authContext.AuthProviderType == AuthProviderType.DeviceCodeProviderFallBack)) { // MSAL scope validation error. Ask customer to specify sign-in audience or tenant Id. throw new MsalClientException(msalServiceEx.ErrorCode, $"{msalServiceEx.Message}.\r\n{ErrorConstants.Message.InvalidScope}", msalServiceEx); } //Something Unknown Went Wrong throw authEx.InnerException ?? authEx; } catch (Exception ex) { throw ex.InnerException ?? ex; } }
protected override void ProcessRecord() { base.ProcessRecord(); AuthConfig authConfig = new AuthConfig { TenantId = TenantId }; CancellationToken cancellationToken = CancellationToken.None; if (ParameterSetName == Constants.UserParameterSet) { // 2 mins timeout. 1 min < HTTP timeout. TimeSpan authTimeout = new TimeSpan(0, 0, Constants.MaxDeviceCodeTimeOut); CancellationTokenSource cts = new CancellationTokenSource(authTimeout); cancellationToken = cts.Token; authConfig.AuthType = AuthenticationType.Delegated; authConfig.Scopes = Scopes ?? new string[] { "User.Read" }; } else { authConfig.AuthType = AuthenticationType.AppOnly; authConfig.ClientId = ClientId; authConfig.CertificateThumbprint = CertificateThumbprint; authConfig.CertificateName = CertificateName; } // Save auth config to session state. SessionState.PSVariable.Set(Constants.GraphAuthConfigId, authConfig); try { // Gets a static instance of IAuthenticationProvider when the client app hasn't changed. IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authConfig); // Incremental scope consent without re-instanciating the auth provider. We will use a static instance. GraphRequestContext graphRequestContext = new GraphRequestContext(); graphRequestContext.CancellationToken = cancellationToken; graphRequestContext.MiddlewareOptions = new Dictionary <string, IMiddlewareOption> { { typeof(AuthenticationHandlerOption).ToString(), new AuthenticationHandlerOption { AuthenticationProviderOption = new AuthenticationProviderOption { Scopes = authConfig.Scopes, ForceRefresh = ForceRefresh } } } }; HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), graphRequestContext); // Trigger consent. authProvider.AuthenticateRequestAsync(httpRequestMessage).GetAwaiter().GetResult(); } catch (AuthenticationException authEx) { if ((authEx.InnerException is TaskCanceledException) && cancellationToken.IsCancellationRequested) { throw new Exception($"Device code terminal timed-out after {Constants.MaxDeviceCodeTimeOut} seconds. Please try again."); } else { throw authEx.InnerException ?? authEx; } } catch (Exception ex) { throw ex.InnerException ?? ex; } WriteObject("Welcome To Microsoft Graph!"); // WriteObject(File.ReadAllText(".\\Art\\WelcomeText.txt")); // WriteObject(File.ReadAllText(".\\Art\\GRaphText.txt")); }
/// <summary> /// Authenticates the client using the provided <see cref="IAuthContext"/>. /// </summary> /// <param name="authContext">The <see cref="IAuthContext"/> to authenticate.</param> /// <param name="forceRefresh">Whether or not to force refresh a token if one exists.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public static async Task <IAuthContext> AuthenticateAsync(IAuthContext authContext, bool forceRefresh, CancellationToken cancellationToken) { try { // Gets a static instance of IAuthenticationProvider when the client app hasn't changed. IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authContext); IClientApplicationBase clientApplication = null; if (authContext.AuthType == AuthenticationType.Delegated) { clientApplication = (authProvider as DeviceCodeProvider).ClientApplication; } if (authContext.AuthType == AuthenticationType.AppOnly) { clientApplication = (authProvider as ClientCredentialProvider).ClientApplication; } // Incremental scope consent without re-instantiating the auth provider. We will use a static instance. GraphRequestContext graphRequestContext = new GraphRequestContext(); graphRequestContext.CancellationToken = cancellationToken; graphRequestContext.MiddlewareOptions = new Dictionary <string, IMiddlewareOption> { { typeof(AuthenticationHandlerOption).ToString(), new AuthenticationHandlerOption { AuthenticationProviderOption = new AuthenticationProviderOption { Scopes = authContext.Scopes, ForceRefresh = forceRefresh } } } }; // Trigger consent. HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), graphRequestContext); await authProvider.AuthenticateRequestAsync(httpRequestMessage); IAccount account = null; if (clientApplication != null) { // Only get accounts when we are using MSAL to get an access token. IEnumerable <IAccount> accounts = clientApplication.GetAccountsAsync().GetAwaiter().GetResult(); account = accounts.FirstOrDefault(); } JwtHelpers.DecodeJWT(httpRequestMessage.Headers.Authorization?.Parameter, account, ref authContext); return(authContext); } catch (AuthenticationException authEx) { if ((authEx.InnerException is TaskCanceledException) && cancellationToken.IsCancellationRequested) { // DeviceCodeTimeout throw new Exception(string.Format( CultureInfo.CurrentCulture, ErrorConstants.Message.DeviceCodeTimeout, Constants.MaxDeviceCodeTimeOut)); } else { throw authEx.InnerException ?? authEx; } } catch (Exception ex) { throw ex.InnerException ?? ex; } }