1

I have a custom policy with an OpenId Connect Technical Profile calling authorize and token endpoints from metadata Items to my custom API middleware which is used to redirect to Apple authenticathion endpoint/website so i can handle a multiApple solution within my custom Policy trying to Ignore client_id and IdTokenAudience. Microsoft documentation states: enter image description here

But unfortunatelly the documentation is wrong and the TokenAudience is always validated after get sucessfully the Apple token and return the flow to B2C through the redirect_uri configured in Apple console for that clientId that I am able to pass through the Authorize endpoint in my API.

Can some B2C expert shed some light about ignore the IdTokenAudience in an OpenId Connect TP inside a Custom Policy?

Microsoft Reference document:

https://learn.microsoft.com/en-us/azure/active-directory-b2c/openid-connect-technical-profile

Thanks in advance!

Juanma Feliu
  • 1,298
  • 4
  • 16

2 Answers2

1

It’s being used as an override.

When this metadata item is not present, we make sure the audience matches the expected audience, client_id.

When it is specified, we make sure the aud claim matches what you state in the metadata item.

This never allows the verification to be turned off.

Jas Suri - MSFT
  • 10,605
  • 2
  • 10
  • 20
  • Thanks Jas. Do you mean the 'aud' claim must match with the client_id metada item when IdTokenAidience is not present? It has no sense and I would like to find a workaround for this problem. Thanks – Juanma Feliu May 15 '22 at 07:36
  • Your understanding is correct. I am not sure why you would not want this behaviour. Can you elaborate on the scenario? – Jas Suri - MSFT May 15 '22 at 07:45
  • 1
    Multitenancy, multiclient scenario to sign-in, signup with apple. – Juanma Feliu May 16 '22 at 11:59
0

Ok, i will answer my question. As stated by @Jas you cannot get rid of client_id because of security validations. So if someone is trying to implement such scenario, i will explain my approach:

  1. Create a Technical Profile like this with client_id="myaudience" and use METADATA item to point to your custom OIDC microservice metadata. A B2C app clientId (B2C URL clientId) must be passed as input claim to handle diferent Apple client_id's depending on registered App:

      <TechnicalProfile Id="AppleID">
       <Protocol Name="OpenIdConnect" />
       <Metadata>
         <Item Key="METADATA">https://xxxxxxxx.ngrok.io/metadata?provider=apple</Item>
         <Item Key="HttpBinding">POST</Item> 
         <Item Key="response_types">code</Item>
         <Item Key="UsePolicyInRedirectUri">false</Item>
         <Item Key="client_id">myaudience</Item>
       </Metadata>
       <CryptographicKeys>
         <Key Id="client_secret" StorageReferenceId="B2C_1A_MultiIDP" />
       </CryptographicKeys>
       <InputClaims>
         <InputClaim ClaimTypeReferenceId="groupId" />
         <InputClaim ClaimTypeReferenceId="appId" PartnerClaimType="clientId" DefaultValue="{OIDC:ClientId}" />
       </InputClaims>
       <OutputClaims>
         <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="sub" />
         <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
         <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="preferred_username" />
         <OutputClaim ClaimTypeReferenceId="displayName" DefaultValue="Apple user" />
         <OutputClaim ClaimTypeReferenceId="picture" PartnerClaimType="picture" />
         <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="apple.com" AlwaysUseDefaultValue="true" />
       </OutputClaims>
     </TechnicalProfile>
    
  2. Your custom multi-IDP microservice will capture in the /authorize endpoint via queryString the clientId and should retrieve from a storage the Apple client_id, teamId, redirect_uri (same than Apple console), KeyId and Issuer corresponding to it.

  3. your /authorize should redirect to the Apple authentication page and will prompt for your credentials.

  4. After enter credentials and click on "continue" button your /token endpoint should be fired, at this point you need to retrieve the .p8 secret (Private Key) from a Key Vault to generate the token to be used with the code in the /token body. The client_id, client_secret and keyId should be overrided with your custom stored data.

enter image description here

  1. After craft the token and build the data to send to Apple /token endpoint, retrieve the Apple final token with its access_token, id_token, refresh_token ..

enter image description here

  1. At this point you need to return all this data to B2C but the audience will fail, so you need to recraft a new id_token before passing the control to B2C through the configured redirect_uri, also you can move the Claims coming from Apple to the new forged token. The secret key used to sign the new token must be configured before as a Policy Key (signature) in your B2C Identity Experience Framewrok. Refer to this doc for more info: https://learn.microsoft.com/en-us/azure/active-directory-b2c/openid-connect-technical-profile This secret is referenced as cryptographickey in the Technical Profile above as: B2C_1A_MultiIDP

enter image description here

  1. Return the data from your microservice /token endpoint to B2C:

enter image description here

Thats all! Happy coding!

Update Jun 2023:

Token Generator class:

public class TokenGenerator : ITokenGenerator
{
    private IConfiguration _config;
    public TokenGenerator(IConfiguration config)
    {
        _config = config;
    }
    public string GenerateGenericToken(string code, string issuer)
    {
        string newToken = string.Empty;

        Dictionary<string, object> claims = new Dictionary<string, object>();
        string sub = Guid.NewGuid().ToString();
        claims.Add("code", code);
        claims.Add("sub", sub);

        claims.Add("email", "dirty@gmail.com");
        claims.Add("email_verified", "true");
        claims.Add("auth_time", ((int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds).ToString());
        newToken = GenerateToken(issuer, claims);
        return newToken;
    }

    public string GenerateToken(string issuer, Dictionary<string, object> claims)
    {
        var B2CEncodedPrivateKey = _config[ConfigParameter.B2CIDPPrivateSecret.ToString()];
        var mySecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(B2CEncodedPrivateKey));
        var myAudience = "MyTPB2CCLIENTID";
        var tokenHandler = new JwtSecurityTokenHandler();

        Dictionary<string, object> header = new Dictionary<string, object>();
        header.Add("kid", "XXXXXXXXXXXXXXXXXXXXXXXX");
        header.Add("kty", "oct");
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, claims["sub"].ToString())
            }),
            Expires = DateTime.UtcNow.AddDays(7),
            Claims = claims,
            Issuer = issuer,
            Audience = myAudience,
            SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256),
            AdditionalHeaderClaims = header

        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

Apple Service:

  public async Task<string> AuthorizeAsync(AuthorizeModelDTO authorizeModel)
        {
            _logger.LogEvent("GENERACION URI PROVIDER <APPLE>");
            string Uri = "";

            AppleClientData clientData = await _context.GetClientDataByProviderAsync(authorizeModel.clientId, authorizeModel.provider);

            Uri = GetAppleAuthorizeUri(authorizeModel.nonce, authorizeModel.state, clientData.TeamId, clientData.ProviderClientId, clientData.redirect_uri, authorizeModel.scope);
            //TODO: Check provider..
            _logger.LogEvent("FIN GENERACION URI PROVIDER <APPLE>");
            return Uri;
        }

        public Task<Dictionary<string, string>> CorrelateAsync(string provider, string code, string clientSecret)
        {
            _logger.LogEvent("GENERACION TOKEN GENERICO");

            Dictionary<string, string> result = new Dictionary<string, string>();  
            if (_config[ConfigParameter.B2CIDPPrivateSecret.ToString()] == clientSecret)
            {
                string issuer = "";
                if (provider.ToLower() == "Apple".ToLower())
                    issuer = "https://appleid.apple.com";

                result.Add("access_token", code);
                result.Add("token_type", "code");
                result.Add("expires_in", DateTime.Now.AddMinutes(30).ToLongTimeString());
                result.Add("refresh_token", "fakerefrshtoken");
                result.Add("id_token", _tokenGenerator.GenerateGenericToken(code, issuer));
            }
            else
            {
                result.Add("error", "invalid client_secret");
            }
            _logger.LogEvent("FIN GENERACION TOKEN GENERICO");
            return Task.FromResult(result);
        }

        public async Task<TokenData> TokenAsync(string clientId, string code, string CorrelationId, string provider, string hostName)
        {
            _logger.LogEvent($"PETICION TOKEN A TRAVES DE CODE <{code}>");

            Dictionary<string, string> result = new Dictionary<string, string>();
            var finalContent = "";
            try
            {
                var prov = provider.ToLower();

                _logger.LogInformation("Buscamos informacion del cliente segun el proveedor");
                //GET cosmos
                AppleClientData clientData = await _context.GetClientDataByProviderAsync(clientId, provider);

                if (clientData != null)
                {
                    _logger.LogInformation("ENCONTRAMOS informacion del cliente segun el proveedor");

                    var appClientId = clientData.ProviderClientId.Replace(".", "");

                    _logger.LogInformation("BUSCAMOS KV Secret");

                    var kvSecret = _config[prov + "-" + appClientId].ToString();

                    if(kvSecret != null && kvSecret != "")
                    {
                        _logger.LogInformation("ENCONTRAMOS KV Secret");
                        var secret = new CreateClientSecret
                        {
                            issuer = clientData.TeamId,
                            keyid = clientData.KeyId,
                            subject = clientData.ProviderClientId,
                            thumb = kvSecret
                        };

                        _logger.LogInformation("GENERAMOS Apple Token");

                        var secretToken = _appleTokenHandler.GenerateAppleToken(_config, "", secret);

                        if (!string.IsNullOrEmpty(secretToken))
                        {
                            _logger.LogInformation("GENERADO Apple Token");

                            
                            var data = _serviceAgent.GetAppleDataBody(clientData.ProviderClientId, secretToken, code, clientData.redirect_uri, CorrelationId);

                            _logger.LogInformation("LLAMADA a Apple /token");
                            finalContent = await _serviceAgent.GetAuthTokenApple(_config, data, "");

                            var r = JsonSerializer.Deserialize<AppleToken>(finalContent);
                            if (r != null)
                            {
                                //Validamos token
                                if (await _tokenValidator.CheckToken(r.id_token))
                                {
                                    TokenData dataToReturn = _appleTokenHandler.DecodeAppleToken(r.id_token);
                                    _logger.LogInformation($"////TOKEN DATA RESULT :");
                                    _logger.LogInformation(dataToReturn.ToString());
                                    _logger.LogEvent($"EXITO FIN PETICION TOKEN A TRAVES DE CODE");
                                    return dataToReturn;
                                }
                            }
                        }
                    } 
                }
            }
            catch (Exception)
            {
                _logger.LogEvent($"ERROR FIN PETICION TOKEN A TRAVES DE CODE");
                throw;
            }
            _logger.LogEvent($"SIN EXITO FIN PETICION TOKEN A TRAVES DE CODE");
            return null;
        }

        private string GetAppleAuthorizeUri(string nonce, string state, string teamId, string AppleclientId, string redirectUri, string scope)
        {
            return $"https://appleid.apple.com/auth/authorize?client_id={AppleclientId}&redirect_uri={redirectUri}&response_type=code&scope={scope}&response_mode=form_post&nonce={nonce}&token_issuer={teamId}&state=StateProperties%3D{state}";
        }
Juanma Feliu
  • 1,298
  • 4
  • 16
  • You need to deal with the correlation between endpoints. If someone is interested i will update my answer. – Juanma Feliu Jun 30 '22 at 09:40
  • Could you share more details about this implementation? In the end is it working properly? I have a similar need, my application has multiple customers wanting to use external IDP's, I'm looking for a solution where I don't have to add one technical profile for each customer everytime. – Rafael Caviquioli Jun 06 '23 at 15:07
  • 1
    Yes, it's working, The example is pointing to the right direction but i'm not sharing all details. You need to fool B2C returning a generic token with the same issuer declared in your Technical Profile "client_id". You need to correlate correctly in your custom policy TP /authorize and /token to your API adding a CorrelationId in your Input Claims. – Juanma Feliu Jun 12 '23 at 07:24
  • Updated answer, Hope it helps a bit more. You can skip "METADATA" endpoint and use "authorize_endpoint" and "token_endpoint" in your TP. – Juanma Feliu Jun 13 '23 at 10:17