csharp/1iveowl/CosmosResourceTokenBroker/src/main/B2CAuthClient/B2CAuthService.cs

B2CAuthService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using B2CAuthClient.Abstract;
using Microsoft.Idensaty.Client;

namespace B2CAuthClient
{
    /// 
    ///     
    ///         An implementation of a Azure AD B2S Authentication Service.
    ///     
    ///     
    ///         Implements IB2CAuthService
    ///     
    /// 
    [Preserve(AllMembers = true)]
    public clast B2CAuthService : IB2CAuthService
    {
        private readonly SemapreplacedSlim _semapreplaced;

        private readonly IPublicClientApplication _pca;
        private readonly IEnumerable _defaultScopes;
        private readonly string _signUpSignInFlowName;

        /// 
        ///     
        ///         Get the current user context of a log-in user.
        ///     
        ///     
        ///         Null if no user has signed in.
        ///     
        ///     
        ///         Only one instance of the authentication service should run i.e. it should be a singleton.
        ///         If 
        ///     
        /// 
        public IUserContext CurrentUserContext { get; private set; }

        public bool IsInterativeSignInInProgress { get; private set; }

        /// 
        ///     
        ///         Creates a new instance of 
        ///     
        /// 
        /// Azure AD B2C Host name
        /// Azure AD B2C Tenant Id
        /// Azure AD B2C Client ID - aka. Application Id
        /// Azure AD B2C Sign In Flow name 
        /// Azure Ad B2C Scopes
        /// iOS Chain Group
        /// Func that returns true at run-time if the device is an Android device.
        /// Func that returns true at run-time if the device is an iOS device.
        /// Func that returns the Parent Window for Android device.
        public B2CAuthService(
            string b2cHostName, 
            string tenantId, 
            string clientId, 
            string signUpSignInFlowName, 
            IEnumerable defaultScopes, 
            string iOsChainGroup, 
            Func isAndroidDeviceFunc,
            Func isAppleDeviceFunc,
            Func getCurrentParentWindowsForAndroidFunc)
        {
            _semapreplaced = new SemapreplacedSlim(1, 1);

            _defaultScopes = defaultScopes;
            _signUpSignInFlowName = signUpSignInFlowName;
            
            var builder = PublicClientApplicationBuilder.Create(clientId)
                .WithB2CAuthority($"https://{b2cHostName}" +
                                  $"/tfp/{tenantId}" +
                                  $"/{signUpSignInFlowName}")
                .WithRedirectUri($"msal{clientId}://auth");

            if (isAndroidDeviceFunc())
            {
                builder.WithParentActivityOrWindow(getCurrentParentWindowsForAndroidFunc);
            }

            if (isAppleDeviceFunc())
            {
                builder.WithIosKeychainSecurityGroup(iOsChainGroup);
            }

            _pca = builder.Build();
        }

        /// 
        ///     
        ///         Sign-in user.
        ///     
        ///     
        ///         Thread-safe synchronous execution. Only one sign-in session will run at a time. 
        ///             /// 
        /// The list of scopes used when logging in.
        /// When set to true, only get user context from cache, do not try interactive log-in.
        /// Cancellation Token
        /// The user context of the logged-in user.
        public async Task SignIn(IEnumerable scopes = null, bool silentlyOnly = false, CancellationToken cancellationToken = default) =>
            await ExecuteSynchronously(async () => await DoSignIn(scopes, silentlyOnly, cancellationToken));
            
        private async Task DoSignIn(IEnumerable scopes = null, bool silentlyOnly = false, CancellationToken cancellationToken = default)
        {
            IUserContext userContext;

            if (scopes is null)
            {
                userContext = await AcquireToken(silentlyOnly, _defaultScopes, cancellationToken);
            }
            else
            {
                userContext = await AcquireToken(silentlyOnly, scopes, cancellationToken);
            }

            CurrentUserContext = userContext;

            return userContext;
        }

        public async Task SignOut(CancellationToken cancellationToken = default)
        {
            if (IsInterativeSignInInProgress)
            {
                throw new B2CAuthClientException("Interactive sign-in session is currently in progress.");
            }

            var account = await GetCachedAccounts();

            if (account is null)
            {
                return;
            }

            await _pca.RemoveAsync(account);
            
            CurrentUserContext = new UserContext();
        }
        
        private async Task AcquireToken(bool silentlyOnly, IEnumerable scopes, CancellationToken ct)
        {
            var account = await GetCachedAccounts();

            try
            {
                if (!(account is null))
                {
                    var authResult = await _pca.AcquireTokenSilent(scopes, account).ExecuteAsync(ct);

                    return new UserContext(authResult);
                }
            }
            catch (MsalUiRequiredException)
            {

            }
            catch (Exception ex)
            {
                throw new B2CAuthClientException("Unhandled B2C Authentication exception", ex);
            }

            if (silentlyOnly)
            {
                throw new B2CAuthClientException(true, "Interactive sign-in is required");
            }

            return await SignInInteractively(account, ct);
        }

        private async Task SignInInteractively(IAccount account, CancellationToken ct)
        {
            try
            {
                IsInterativeSignInInProgress = true;

                AuthenticationResult authResult;

                if (account is null)
                {
                    authResult = await _pca.AcquireTokenInteractive(_defaultScopes)
                        .ExecuteAsync(ct);
                }
                else
                {
                    authResult = await _pca.AcquireTokenInteractive(_defaultScopes)
                        .WithAccount(account)
                        .ExecuteAsync(ct);
                }

                return new UserContext(authResult);
            }
            catch (Exception ex)
            {
                throw new B2CAuthClientException($"Unable to acquire token interactive. Unhandled exception: {ex}");
            }
            finally
            {
                IsInterativeSignInInProgress = false;
            }
        }

        private async Task GetCachedAccounts()
        {
            var accounts = await _pca.GetAccountsAsync();
            
            return accounts.FirstOrDefault(a =>
                a.HomeAccountId?.ObjectId?.Split('.')?[0]?.EndsWith(_signUpSignInFlowName.ToLowerInvariant()) ?? false);
        }

        private async Task ExecuteSynchronously(Func task)
        {
            await _semapreplaced.WaitAsync().ConfigureAwait(false);

            try
            {
                return await task();
            }
            finally
            {
                _semapreplaced.Release();
            }

        }

        private async Task ExecuteSynchronously(Func task)
        {
            await _semapreplaced.WaitAsync().ConfigureAwait(false);

            try
            {
                await task();
            }
            finally
            {
                _semapreplaced.Release();
            }
        }
    }
}