Support identity federation from Entra ID #175
Replies: 2 comments 2 replies
-
Unfortunately currently we don't plan for features that are specific to third party products. |
Beta Was this translation helpful? Give feedback.
-
We would also be interested in something like this. As far as I can tell, almost all of the major identity providers support some sort of federation with another identity provider. The use cases are very common for non-human identities (such as CI/CD pipelines). Examples: That said, I do agree this is not standardized and likely very use-case specific. The three implementations I linked are all very different in behavior and functionality even if they all allow exchanging a token from one identity provider with another. For @diego-parra-celonis, it sounds like maybe our use case might be similar to yours. We have implemented this ourselves with a custom /// <summary>
/// Validates the incoming secret of the client_credentials flow is from a trusted OpenID Connect provider.
/// This is to allow clients to exchange an Azure Active Directory token for a ULS token.
/// </summary>
public class FederatedOpenIDConnectCredentialValidator : ISecretValidator
{
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _OpenIdConfigCache = new ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>>();
private readonly ILogger _logger;
private readonly ISecurityTokenValidator _tokenValidator;
public FederatedOpenIDConnectCredentialValidator(ILogger<FederatedOpenIDConnectCredentialValidator> logger)
{
_logger = logger;
_tokenValidator = new JwtSecurityTokenHandler();
}
public async Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret)
{
var fail = new SecretValidationResult { Success = false };
var success = new SecretValidationResult { Success = true };
if (parsedSecret.Type != IdentityServerConstants.ParsedSecretTypes.JwtBearer)
{
return fail;
}
if (parsedSecret.Credential is not string jwtTokenString)
{
_logger.LogError("ParsedSecret.Credential is not a string.");
return fail;
}
var subject = GetSubjectFromToken(jwtTokenString);
if (string.IsNullOrEmpty(subject))
{
_logger.LogError("JWT is missing a subject claim.");
return fail;
}
var federatedCred = GetFederatedCredentialWithSubject(secrets, subject);
if (federatedCred == null)
{
_logger.LogError("No federated credential has a subject matching the {subject}.", subject);
return fail;
}
if (!await ValidateOpenIdConnectJwt(federatedCred, jwtTokenString))
{
return fail;
}
return success;
}
private async Task<bool> ValidateOpenIdConnectJwt(FederatedOpenIDConnectCredential credential, string jwtString)
{
var keys = await GetSigningKeys(credential);
var tokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = credential.Issuer,
ValidAudience = credential.Audience,
IssuerSigningKeys = keys,
};
try
{
_tokenValidator.ValidateToken(jwtString, tokenValidationParameters, out var _);
return true;
}
catch (Exception e)
{
_logger.LogError(e, "Failed to validate OpenID Connect JWT with issuer {issuer} and audience {audience}", credential.Issuer, credential.Audience);
}
return false;
}
private async Task<ICollection<SecurityKey>> GetSigningKeys(FederatedOpenIDConnectCredential credential)
{
var openIdConfig = _OpenIdConfigCache.GetOrAdd(
credential.Issuer,
(issuer) => new ConfigurationManager<OpenIdConnectConfiguration>(
$"{issuer}/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever()));
var discoveryDocument = await openIdConfig.GetConfigurationAsync();
return discoveryDocument.SigningKeys;
}
private FederatedOpenIDConnectCredential GetFederatedCredentialWithSubject(IEnumerable<Secret> secrets, string subject)
{
foreach (var secret in secrets)
{
if (secret is FederatedOpenIDConnectCredential federatedCredential)
{
if (federatedCredential.Subject == subject)
{
return federatedCredential;
}
}
}
return null;
}
private string GetSubjectFromToken(string token)
{
try
{
var jwt = new JwtSecurityToken(token);
return jwt.Subject;
}
catch
{
_logger.LogWarning("Could not parse client assertion");
return null;
}
}
} /// <summary>
/// This is a secret that is used to authenticate a user with a federated OpenID Connect identity provider.
/// It is solely intended to be used to allow a client to authenticate with a Azure Active Directory token.
/// </summary>
public class FederatedOpenIDConnectCredential : Secret
{
public string Issuer { get; set; }
public string Audience { get; set; }
public string Subject { get; set; }
public FederatedOpenIDConnectCredential(string issuer, string audience, string subject) : base()
{
Issuer = issuer;
Audience = audience;
Subject = subject;
Value = $"{Issuer}|{Audience}|{Subject}";
Type = Constants.SecretTypes.FederatedOpenIDConnectCredential;
}
/// <summary>
/// Created a FederatedOpenIDConnectCredential from a AzureADApplicationName and AzureADObjectID using defaults for Issuer and Audience.
/// Note that the AzureADObjectID is the Object ID of the Enterprise Application in Azure AD.
/// We require AzureADApplicationName even though it is unused for documentation purposes when creating these secrets.
/// </summary>
/// <param name="azureADApplication"></param>
/// <param name="config"></param>
/// <returns></returns>
public static FederatedOpenIDConnectCredential FromAzureADEnterpriseApplication(string AzureADApplicationName, string AzureADObjectID, IConfiguration config)
{
return new FederatedOpenIDConnectCredential(
config[Constants.FederatedOpenIDCredential_AzureADIssuer],
config[Constants.FederatedOpenIDCredential_AzureADAudience],
AzureADObjectID);
}
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi
We're currently trying to solve a situation where one of our customers uses heavily Azure as their cloud provider, and they use manage identities for their applications, because it allows to avoid defining secrets at all, therefore is highly secure.
Of course this works very well when they are inside Azure, but when a service is outside Azure or is not authenticating with the same Entra ID, as we operate as a separate SaSS company, we struggle because if they want to access our REST Api, they need to create a new Auth Client in our app, get a client secret and it's not secure enough for them.
Another option is to use certificates to create a client assertion, which is something that is supported by Duende already https://docs.duendesoftware.com/identityserver/tokens/client-authentication#authentication-using-a-private-key-jwt , but they mention that if they forget to rotate a certificate, it may lead to downtimes, so it's still insecure.
Their preferred choice is to support some sort of OIDC federation, similar to what is described here: https://feng.lu/2024/09/18/How-to-secretless-access-Azure-and-AWS-resources-with-Azure-managed-identity-and-AWS-IAM/
The general idea is to receive a token generated by Entra ID for a given app which has a system managed-identity, and intents to invoke an external service, it could be difficult to use this token in our application as we have our own claims, and we trust only tokens issued by our IdP (backed up by Duende), but of course if there's a configuration in place, our IdP could trust and validate this token and issue a new one, similar to https://datatracker.ietf.org/doc/html/rfc8693
We know that this is possible by using an extension validator, but it would be much better if this is supported out of the box as companies should not try to tweak or implement security protocols, and this could help the product to support secret-less scenarios which will be more common each day.
Beta Was this translation helpful? Give feedback.
All reactions