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();
}
}
}
}