Skip to content

Commit

Permalink
Initial implementation of CIAM support (#4054)
Browse files Browse the repository at this point in the history
* Initial prototype for Ciam integration

* Ciam updates

* Update

* Changing login to tenant in Ciam tranformation

* Updates

* Adding exception for CIAM authority in request.
Adding additional tests
Refactoring

* Update Comments

* typo

* Update

* Refactoring

* Updating dev app

* Adding additional tests
Refactoring.

---------

Co-authored-by: trwalke <trwalke@microsoft.com>
  • Loading branch information
trwalke and trwalke committed Apr 13, 2023
1 parent 300fba1 commit 275c373
Show file tree
Hide file tree
Showing 16 changed files with 1,309 additions and 883 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public T WithAuthority(string authorityUri, bool validateAuthority = true)
{
throw new ArgumentNullException(nameof(cloudInstanceUri));
}
CommonParameters.AuthorityOverride = AuthorityInfo.FromAadAuthority(new Uri(cloudInstanceUri), tenantId, validateAuthority);
CommonParameters.AuthorityOverride = AuthorityInfo.FromAadAuthority(cloudInstanceUri, tenantId, validateAuthority);
return this as T;
}

Expand Down Expand Up @@ -164,7 +164,7 @@ public T WithAuthority(string authorityUri, bool validateAuthority = true)
{
throw new ArgumentNullException(nameof(cloudInstanceUri));
}
CommonParameters.AuthorityOverride = AuthorityInfo.FromAadAuthority(new Uri(cloudInstanceUri), tenant, validateAuthority);
CommonParameters.AuthorityOverride = AuthorityInfo.FromAadAuthority(cloudInstanceUri, tenant, validateAuthority);
return this as T;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ protected virtual void Validate()

internal void ValidateAndCalculateApiId()
{
if (CommonParameters.AuthorityOverride?.AuthorityType == AuthorityType.Ciam)
{
throw new MsalClientException(MsalError.SetCiamAuthorityAtRequestLevelNotSupported, MsalErrorMessage.SetCiamAuthorityAtRequestLevelNotSupported);
}

Validate();
CommonParameters.ApiId = CalculateApiEventId();
CommonParameters.CorrelationId = CommonParameters.UseCorrelationIdFromUser ? CommonParameters.UserProvidedCorrelationId : Guid.NewGuid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ public T WithAuthority(string authorityUri, bool validateAuthority = true)
}

var authorityInfo = AuthorityInfo.FromAadAuthority(
new Uri(cloudInstanceUri),
cloudInstanceUri,
tenant,
validateAuthority);
Config.Authority = new AadAuthority(authorityInfo);
Expand Down
73 changes: 47 additions & 26 deletions src/client/Microsoft.Identity.Client/AppConfig/AuthorityInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.Instance;
using Microsoft.Identity.Client.Instance.Validation;
using Microsoft.Identity.Client.Internal;
Expand Down Expand Up @@ -40,7 +41,7 @@ internal class AuthorityInfo
switch (AuthorityType)
{
case AuthorityType.Generic:
CanonicalAuthority = new Uri(authorityUri.ToString());
CanonicalAuthority = authorityUri;
break;

case AuthorityType.B2C:
Expand Down Expand Up @@ -98,7 +99,6 @@ internal class AuthorityInfo
"https://{0}/common/userrealm/",
Host),
authorityUri.Port);

break;
}
}
Expand Down Expand Up @@ -145,34 +145,47 @@ internal class AuthorityInfo
#region Builders
internal static AuthorityInfo FromAuthorityUri(string authorityUri, bool validateAuthority)
{
string canonicalUri = CanonicalizeAuthorityUri(authorityUri);
ValidateAuthorityUri(canonicalUri);
string canonicalAuthority = CanonicalizeAuthorityUri(authorityUri);

Uri canonicalAuthorityUri = ValidateAndCreateAuthorityUri(canonicalAuthority);

var authorityType = GetAuthorityType(canonicalUri);
canonicalAuthorityUri = TransformIfCiamAuthority(canonicalAuthorityUri);

var authorityType = GetAuthorityType(canonicalAuthorityUri);

// Authority validation is only supported for AAD
if (authorityType == AuthorityType.B2C || authorityType == AuthorityType.Generic)
{
validateAuthority = false;
}

return new AuthorityInfo(authorityType, canonicalUri, validateAuthority);
return new AuthorityInfo(authorityType, canonicalAuthorityUri, validateAuthority);
}

internal static AuthorityInfo FromAadAuthority(Uri cloudInstanceUri, Guid tenantId, bool validateAuthority)
private static Uri TransformIfCiamAuthority(Uri authorityUri)
{
if (isCiamAuthority(authorityUri))
{
return CiamAuthority.TransformAuthority(authorityUri);
}

return authorityUri;
}

internal static AuthorityInfo FromAadAuthority(string cloudInstanceUri, Guid tenantId, bool validateAuthority)
{
#pragma warning disable CA1305 // Specify IFormatProvider
return FromAuthorityUri(new Uri(cloudInstanceUri, tenantId.ToString("D")).AbsoluteUri, validateAuthority);
return FromAuthorityUri($"{cloudInstanceUri.Trim().TrimEnd('/')}/{tenantId.ToString("D")}", validateAuthority);
#pragma warning restore CA1305 // Specify IFormatProvider
}

internal static AuthorityInfo FromAadAuthority(Uri cloudInstanceUri, string tenant, bool validateAuthority)
internal static AuthorityInfo FromAadAuthority(string cloudInstanceUri, string tenant, bool validateAuthority)
{
if (Guid.TryParse(tenant, out Guid tenantId))
{
return FromAadAuthority(cloudInstanceUri, tenantId, validateAuthority);
}
return FromAuthorityUri(new Uri(cloudInstanceUri, tenant).AbsoluteUri, validateAuthority);
return FromAuthorityUri($"{cloudInstanceUri.Trim().TrimEnd('/')}/{tenant}", validateAuthority);
}

internal static AuthorityInfo FromAadAuthority(
Expand Down Expand Up @@ -315,6 +328,9 @@ internal Authority CreateAuthority()
case AuthorityType.Dsts:
return new DstsAuthority(this);

case AuthorityType.Ciam:
return new CiamAuthority(this);

case AuthorityType.Generic:
return new GenericAuthority(this);

Expand All @@ -327,7 +343,7 @@ internal Authority CreateAuthority()

#endregion

private static void ValidateAuthorityUri(string authority, AuthorityType? authorityType = null)
private static Uri ValidateAndCreateAuthorityUri(string authority, AuthorityType? authorityType = null)
{
if (string.IsNullOrWhiteSpace(authority))
{
Expand All @@ -340,13 +356,14 @@ private static void ValidateAuthorityUri(string authority, AuthorityType? author
}

var authorityUri = new Uri(authority);

if (authorityUri.Scheme != "https")
{
throw new ArgumentException(MsalErrorMessage.AuthorityUriInsecure, nameof(authority));
}

string path = authorityUri.AbsolutePath.Substring(1);
if (string.IsNullOrWhiteSpace(path))
if (string.IsNullOrWhiteSpace(path) && !isCiamAuthority(authorityUri))
{
throw new ArgumentException(MsalErrorMessage.AuthorityUriInvalidPath, nameof(authority));
}
Expand All @@ -359,6 +376,8 @@ private static void ValidateAuthorityUri(string authority, AuthorityType? author
throw new ArgumentException(MsalErrorMessage.AuthorityUriInvalidPath);
}
}

return authorityUri;
}

private static string GetAuthorityUri(
Expand All @@ -372,12 +391,6 @@ private static void ValidateAuthorityUri(string authority, AuthorityType? author
return string.Format(CultureInfo.InvariantCulture, "{0}/{1}", cloudUrl, tenantValue);
}

internal static string GetFirstPathSegment(string authority)
{
var uri = new Uri(authority);
return GetFirstPathSegment(uri);
}

internal static string GetFirstPathSegment(Uri authority)
{
if (authority.Segments.Length >= 2)
Expand All @@ -389,12 +402,6 @@ internal static string GetFirstPathSegment(Uri authority)
throw new InvalidOperationException(MsalErrorMessage.AuthorityDoesNotHaveTwoSegments);
}

internal static string GetSecondPathSegment(string authority)
{
var uri = new Uri(authority);
return GetSecondPathSegment(uri);
}

internal static string GetSecondPathSegment(Uri authority)
{
if (authority.Segments.Length >= 3)
Expand All @@ -406,9 +413,14 @@ internal static string GetSecondPathSegment(Uri authority)
throw new InvalidOperationException(MsalErrorMessage.DstsAuthorityDoesNotHaveThreeSegments);
}

private static AuthorityType GetAuthorityType(string authority)
private static AuthorityType GetAuthorityType(Uri authorityUri)
{
string firstPathSegment = GetFirstPathSegment(authority);
if (isCiamAuthority(authorityUri))
{
return AuthorityType.Ciam;
}

string firstPathSegment = GetFirstPathSegment(authorityUri);

if (string.Equals(firstPathSegment, "adfs", StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -428,6 +440,11 @@ private static AuthorityType GetAuthorityType(string authority)
return AuthorityType.Aad;
}

private static bool isCiamAuthority(Uri authorityUri)
{
return authorityUri.Host.EndsWith(Constants.CiamAuthorityHostSuffix);
}

private static string[] GetPathSegments(string absolutePath)
{
string[] pathSegments = absolutePath.Substring(1).Split(
Expand Down Expand Up @@ -455,6 +472,7 @@ public static IAuthorityValidator CreateAuthorityValidator(AuthorityInfo authori
return new AadAuthorityValidator(requestContext);
case AuthorityType.B2C:
case AuthorityType.Dsts:
case AuthorityType.Ciam:
case AuthorityType.Generic:
return new NullAuthorityValidator();
default:
Expand Down Expand Up @@ -511,6 +529,9 @@ public static IAuthorityValidator CreateAuthorityValidator(AuthorityInfo authori
case AuthorityType.B2C:
return new B2CAuthority(nonNullAuthInfo);

case AuthorityType.Ciam:
return new CiamAuthority(nonNullAuthInfo);

case AuthorityType.Generic:
return new GenericAuthority(nonNullAuthInfo);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal enum AuthorityType

Dsts,

Generic
Generic,

Ciam
}
}
43 changes: 43 additions & 0 deletions src/client/Microsoft.Identity.Client/Instance/CiamAuthority.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Internal;

namespace Microsoft.Identity.Client.Instance
{
internal class CiamAuthority : AadAuthority
{
internal CiamAuthority(AuthorityInfo authorityInfo) :
base(authorityInfo)
{ }

/// <summary>
/// Translates CIAM authorities into a usable form. This is needed only until ESTS is updated to support the north star format
/// North star format: https://idgciamdemo.ciamlogin.com
/// Transformed format: https://idgciamdemo.ciamlogin.com/idgciamdemo.onmicrosoft.com
/// </summary>
internal static Uri TransformAuthority(Uri ciamAuthority)
{
string transformedInstance;
string transformedTenant;

string host = ciamAuthority.Host + ciamAuthority.AbsolutePath;
if (string.Equals(ciamAuthority.AbsolutePath, "/"))
{
string ciamTenant = host.Substring(0, host.IndexOf(Constants.CiamAuthorityHostSuffix, StringComparison.OrdinalIgnoreCase));
transformedInstance = $"https://{ciamTenant}{Constants.CiamAuthorityHostSuffix}/";
transformedTenant = ciamTenant + ".onmicrosoft.com";
return new Uri(transformedInstance + transformedTenant);
}
else
{
return ciamAuthority;
}
}
}
}
1 change: 1 addition & 0 deletions src/client/Microsoft.Identity.Client/Internal/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ internal static class Constants
public const string ManagedIdentityResourceId = "mi_res_id";
public const string ManagedIdentityDefaultClientId = "system_assigned_managed_identity";
public const string ManagedIdentityDefaultTenant = "managed_identity";
public const string CiamAuthorityHostSuffix = ".ciamlogin.com";

public static string FormatEnterpriseRegistrationOnPremiseUri(string domain)
{
Expand Down
5 changes: 5 additions & 0 deletions src/client/Microsoft.Identity.Client/MsalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,5 +1141,10 @@ public static class MsalError
/// Using combined flat storage, like a file, to store both app and user tokens is not supported. Use a partitioned token cache (for ex. distributed cache like Redis) or separate files for app and user token caches. See https://aka.ms/msal-net-token-cache-serialization .
/// </summary>
public const string CombinedUserAppCacheNotSupported = "combined_user_app_cache_not_supported";

/// <summary>
/// Setting the CIAM authority (ex. "{tenantName}.ciamlogin.com") at the request level is not supported. The CIAM authority must be set during application creation.
/// </summary>
public const string SetCiamAuthorityAtRequestLevelNotSupported = "set_ciam_authority_at_request_level_not_supported";
}
}
1 change: 1 addition & 0 deletions src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,5 +457,6 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName)
public const string ManagedIdentityUserAssignedNotConfigurableAtRuntime = "[Managed Identity] Service Fabric user assigned managed identity ClientId or ResourceId is not configurable at runtime.";
public const string CombinedUserAppCacheNotSupported = "Using a combined flat storage, like a file, to store both app and user tokens is not supported. Use a partitioned token cache (for ex. distributed cache like Redis) or separate files for app and user token caches. See https://aka.ms/msal-net-token-cache-serialization .";
public const string JsonParseErrorMessage = "There was an error parsing the response from the token endpoint, see inner exception for details. Verify that your app is configured correctly. If this is a B2C app, one possible cause is acquiring a token for Microsoft Graph, which is not supported. See https://aka.ms/msal-net-up";
public const string SetCiamAuthorityAtRequestLevelNotSupported = "Setting the CIAM authority (ex. \"{tenantName}.ciamlogin.com\") at the request level is not supported. The CIAM authority must be set during application creation";
}
}
1 change: 1 addition & 0 deletions tests/Microsoft.Identity.Test.Common/TestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ public static HashSet<string> s_scope
public const string iOSBrokerErrDescr = "Test Error Description";
public const string iOSBrokerErrorMetadata = "error_metadata";
public const string iOSBrokerErrorMetadataValue = @"{""home_account_id"":""test_home"", ""username"" : """ + Username + @""" }";
public const string DefaultGraphScope = "https://graph.windows.net/.default";

//This value is only for testing purposes. It is for a certificate that is not used for anything other than running tests
public const string _defaultx5cValue = @"MIIDHzCCAgegAwIBAgIQM6NFYNBJ9rdOiK+C91ZzFDANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDExVBQ1MyQ2xpZW50Q2VydGlmaWNhdGUwHhcNMTIwNTIyMj
Expand Down

0 comments on commit 275c373

Please sign in to comment.