Base solution for your next web application
Open Closed

Azure B2C OpenId new user login failing #9789


User avatar
0
rmorris created

Prerequisites

  • What is your product version? 8.9.0 (I think, it's nuget package version 2.1.1 publish date 2020-06-18)
  • What is your product type (Angular or MVC)? Angular
  • What is product framework type (.net framework or .net core)? .net core

If issue related with ABP Framework

  • What is ABP Framework version? nuget is 5.10.1 publish date 2020-07-01

We're trying to configure the server to work with an Azure B2C tenant using the OpenId section of the appsettings and we get this exception thrown when the token comes back in oursln.Portal.Web.Controllers.TokenAuthController:

{
  "ClassName": "System.InvalidOperationException",
  "Message": "IDX20803: Unable to obtain configuration from: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.",
  "Data": null,
  "InnerException": {
    "ClassName": "System.IO.IOException",
    "Message": "IDX20807: Unable to retrieve document from: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. HttpResponseMessage: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]', HttpResponseMessage.Content: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.",
    "Data": null,
    "InnerException": null,
    "HelpURL": null,
    "StackTraceString": "   at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel)\r\n   at Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(String address, IDocumentRetriever retriever, CancellationToken cancel)\r\n   at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)",
    "RemoteStackTraceString": null,
    "RemoteStackIndex": 0,
    "ExceptionMethod": null,
    "HResult": -2146232800,
    "Source": "Microsoft.IdentityModel.Protocols",
    "WatsonBuckets": null
  },
  "HelpURL": null,
  "StackTraceString": "   at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)\r\n   at Abp.AspNetZeroCore.Web.Authentication.External.OpenIdConnect.OpenIdConnectAuthProviderApi.ValidateToken(String token, String issuer, IConfigurationManager`1 configurationManager, CancellationToken ct)\r\n   at Abp.AspNetZeroCore.Web.Authentication.External.OpenIdConnect.OpenIdConnectAuthProviderApi.GetUserInfo(String token)\r\n   at oursln.Portal.Web.Controllers.TokenAuthController.GetExternalUserInfo(ExternalAuthenticateModel model) in C:\\Users\\rmorris\\source\\repos\\oursln-portal-server\\src\\oursln.Portal.Web.Core\\Controllers\\TokenAuthController.cs:line 516",
  "RemoteStackTraceString": null,
  "RemoteStackIndex": 0,
  "ExceptionMethod": null,
  "HResult": -2146233079,
  "Source": "Microsoft.IdentityModel.Protocols",
  "WatsonBuckets": null
}

Is this the ClaimsMapping issue? No matter what we have there we get this... here's what an attempt at the config looks like:

    "OpenId": {
      "IsEnabled": "true",
      "ClientId": "<clientId>",
      "Authority": "https://<tenantname>.b2clogin.com/<tenantId>/v2.0/",
      "LoginUrl": "https://<tenantname>.b2clogin.com/<tenantname>.onmicrosoft.com/<policy>/oauth2/v2.0/authorize",
      "ValidateIssuer": "false",
      "ClaimsMapping": [
        {
          "claim": "unique_name",
          "key": "name"
        }
      ]
    },

We'd prefer using the object ID or the email from the token but for now we're just trying to get something to work. Here's what's in the token during that exception:

{
  "exp": 1603902874,
  "nbf": 1603899274,
  "ver": "1.0",
  "iss": "https://<tenantname>.b2clogin.com/<tenantId>/v2.0/",
  "sub": "<objectId>",
  "aud": "<clientId>",
  "nonce": "<nonce>",
  "iat": 1603899274,
  "auth_time": 1603899274,
  "family_name": "<lastname>",
  "given_name": "<firstname>",
  "name": "<displayname>",
  "emails": [
    "<email>"
  ],
  "tfp": "<policy>"
}

I've seen code modifying oursln.Portal.Web.Startup.AuthConfigurer, to deal with the email list in the returned token at a minimum, along with other customizations. I've tried changing that, to no effect. No matter what between the config and the auth configurer that exception is always thrown there. Could you offer any suggestions where to get started on this?


6 Answer(s)
  • User Avatar
    0
    rmorris created

    Well, upon finding out about the logging setting for IdentityModelEventSource.ShowPII, the exception has more info:

    {
      "ClassName": "System.InvalidOperationException",
      "Message": "IDX20803: Unable to obtain configuration from: 'https://<tenantname>.b2clogin.com/<tenantId>/v2.0//.well-known/openid-configuration'.",
      "Data": null,
      "InnerException": {
        "ClassName": "System.IO.IOException",
        "Message": "IDX20807: Unable to retrieve document from: 'https://<tenantname>.b2clogin.com/<tenantId>/v2.0//.well-known/openid-configuration'. HttpResponseMessage: 'StatusCode: 404, ReasonPhrase: 'Not Found', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:\r\n{\r\n  X-Frame-Options: DENY\r\n  Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n  X-Content-Type-Options: nosniff\r\n  X-XSS-Protection: 1; mode=block\r\n  Date: Wed, 28 Oct 2020 18:20:32 GMT\r\n  Content-Type: text/html\r\n  Content-Length: 1245\r\n}', HttpResponseMessage.Content: '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\"/>\r\n<title>404 - File or directory not found.</title>\r\n<style type=\"text/css\">\r\n<!--\r\nbody{margin:0;font-size:.7em;font-family:Verdana, Arial, Helvetica, sans-serif;background:#EEEEEE;}\r\nfieldset{padding:0 15px 10px 15px;} \r\nh1{font-size:2.4em;margin:0;color:#FFF;}\r\nh2{font-size:1.7em;margin:0;color:#CC0000;} \r\nh3{font-size:1.2em;margin:10px 0 0 0;color:#000000;} \r\n#header{width:96%;margin:0 0 0 0;padding:6px 2% 6px 2%;font-family:\"trebuchet MS\", Verdana, sans-serif;color:#FFF;\r\nbackground-color:#555555;}\r\n#content{margin:0 0 0 2%;position:relative;}\r\n.content-container{background:#FFF;width:96%;margin-top:8px;padding:10px;position:relative;}\r\n-->\r\n</style>\r\n</head>\r\n<body>\r\n<div id=\"header\"><h1>Server Error</h1></div>\r\n<div id=\"content\">\r\n <div class=\"content-container\"><fieldset>\r\n  <h2>404 - File or directory not found.</h2>\r\n  <h3>The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.</h3>\r\n </fieldset></div>\r\n</div>\r\n</body>\r\n</html>\r\n'.",
        "Data": null,
        "InnerException": null,
        "HelpURL": null,
        "StackTraceString": "   at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel)\r\n   at Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(String address, IDocumentRetriever retriever, CancellationToken cancel)\r\n   at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)",
        "RemoteStackTraceString": null,
        "RemoteStackIndex": 0,
        "ExceptionMethod": null,
        "HResult": -2146232800,
        "Source": "Microsoft.IdentityModel.Protocols",
        "WatsonBuckets": null
      },
      "HelpURL": null,
      "StackTraceString": "   at Microsoft.IdentityModel.Protocols.ConfigurationManager`1.GetConfigurationAsync(CancellationToken cancel)\r\n   at Abp.AspNetZeroCore.Web.Authentication.External.OpenIdConnect.OpenIdConnectAuthProviderApi.ValidateToken(String token, String issuer, IConfigurationManager`1 configurationManager, CancellationToken ct)\r\n   at Abp.AspNetZeroCore.Web.Authentication.External.OpenIdConnect.OpenIdConnectAuthProviderApi.GetUserInfo(String token)\r\n   at oursln.Portal.Web.Controllers.TokenAuthController.GetExternalUserInfo(ExternalAuthenticateModel model) in C:\\Users\\rmorris\\source\\repos\\oursln-portal-server\\src\\oursln.Portal.Web.Core\\Controllers\\TokenAuthController.cs:line 516",
      "RemoteStackTraceString": null,
      "RemoteStackIndex": 0,
      "ExceptionMethod": null,
      "HResult": -2146233079,
      "Source": "Microsoft.IdentityModel.Protocols",
      "WatsonBuckets": null
    }
    

    The token's issuer is

    https://&lt;tenantname&gt;.b2clogin.com/&lt;tenantId&gt;/v2.0/
    

    and the metadata is at

    https://&lt;tenantname&gt;.b2clogin.com/&lt;tenantname&gt;.onmicrosoft.com/&lt;policy&gt;/v2.0/.well-known/openid-configuration
    

    . So it appears we need to edit the AuthConfigurer and provide for the differing shape of the expected metadata URL for this particular tenant, it doesn't match the OTB config. Something along the lines of what was discussed here, right?

  • User Avatar
    0
    rmorris created

    So my changes to AuthConfigurer don't seem to take effect. It doesn't use the metadata address I specified in the config, and I would assume OpenIdConnectAuthProviderApi is what is setting up the concat of the authority setting in config to the rest of the metadata address. Is there a way to modify this or override this behavior?

    Or is there another problem, like our tenant/policy is misconfigured?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @rmorris

    OpenIdConnectAuthProviderApi just adds "/.well-known/openid-configuration" to the end of your "Authority" value in appsettings.json file. So, can you just set Authority as below in your appsettings.json file ?

    https://<tenantname>.b2clogin.com/<tenantname>.onmicrosoft.com/<policy>/v2.0
    
  • User Avatar
    0
    rmorris created

    No, as the issuer in the token is

    https://<tenantname>.b2clogin.com/<tenantId>/v2.0/
    

    Without the authority set to that the frontend immediately rejects the login request, and the backend probably would fail verification too. I wound up having to dredge up an old copy of OpenIdConnectAuthProviderApi found on the forums here and adapted it for the current code base inheriting from ExternalAuthProviderApiBase. As well as modifying the PortalWebHostModule's ConfigureExternalAuthProviders()'s OpenId section for a new IExternalLoginInfoProvider that passes along a MetadataUrl.

    To summarize the problem the authority is the B2C tenant but the key belongs to the user flow/custom policy.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    This is the latest version of OpenIdConnectAuthProviderApi in case if you need it. Could you share your modified version ? It can help me to understand the problem in a better way. Thanks.

    using System;
    using System.IdentityModel.Tokens.Jwt;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading;
    using System.Threading.Tasks;
    using Abp.UI;
    using Microsoft.IdentityModel.Protocols;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Microsoft.IdentityModel.Tokens;
    
    namespace Abp.AspNetZeroCore.Web.Authentication.External.OpenIdConnect
    {
        public class OpenIdConnectAuthProviderApi : ExternalAuthProviderApiBase
        {
            public const string Name = "OpenIdConnect";
    
            public async override Task<ExternalAuthUserInfo> GetUserInfo(string token)
            {
                var issuer = ProviderInfo.AdditionalParams["Authority"];
                if (string.IsNullOrEmpty(issuer))
                {
                    throw new ApplicationException("Authentication:OpenId:Issuer configuration is required.");
                }
    
                var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
                    issuer + "/.well-known/openid-configuration",
                    new OpenIdConnectConfigurationRetriever(),
                    new HttpDocumentRetriever());
    
                var validatedTokenResult = await ValidateToken(token, issuer, configurationManager);
    
                var fullNameClaim = validatedTokenResult.Principal.Claims.FirstOrDefault(c => c.Type == "name");
    
                if (fullNameClaim == null)
                {
                    throw new UserFriendlyException("name claim is missing !");
                }
    
                var emailClaim = validatedTokenResult.Principal.Claims.FirstOrDefault(c => c.Type == "unique_name");
    
                if (emailClaim == null)
                {
                    throw new UserFriendlyException("unique_name claim is missing !");
                }
    
                var fullNameParts = fullNameClaim.Value.Split(' ');
    
                return new ExternalAuthUserInfo
                {
                    Provider = Name,
                    ProviderKey = validatedTokenResult.Token.Subject,
                    Name = fullNameParts[0],
                    Surname = fullNameParts.Length > 1 ? fullNameParts[1] : fullNameParts[0],
                    EmailAddress = emailClaim.Value
                };
            }
    
            private async Task<ValidateTokenResult> ValidateToken(
                string token,
                string issuer,
                IConfigurationManager<OpenIdConnectConfiguration> configurationManager,
                CancellationToken ct = default(CancellationToken))
            {
                if (string.IsNullOrEmpty(token))
                {
                    throw new ArgumentNullException(nameof(token));
                }
    
                if (string.IsNullOrEmpty(issuer))
                {
                    throw new ArgumentNullException(nameof(issuer));
                }
    
                var discoveryDocument = await configurationManager.GetConfigurationAsync(ct);
                var signingKeys = discoveryDocument.SigningKeys;
    
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = bool.Parse(ProviderInfo.AdditionalParams["ValidateIssuer"]),
                    ValidIssuer = issuer,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKeys = signingKeys,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.FromMinutes(5),
                    ValidateAudience = false
                };
    
                var principal = new JwtSecurityTokenHandler().ValidateToken(token, validationParameters, out var rawValidatedToken);
                principal.AddMappedClaims(ProviderInfo.ClaimMappings);
    
                var audienceClaim = principal.Claims.FirstOrDefault(c => c.Type == "aud");
    
                if (audienceClaim == null)
                {
                    throw new UserFriendlyException("aud claim is missing !");
                }
    
                //Validate clientId
                if (ProviderInfo.ClientId != audienceClaim.Value)
                {
                    throw new ApplicationException("ClientId couldn't verified.");
                }
    
                return new ValidateTokenResult((JwtSecurityToken)rawValidatedToken, principal);
            }
    
            class ValidateTokenResult
            {
                public JwtSecurityToken Token { get; set; }
    
                public ClaimsPrincipal Principal { get; set; }
    
                public ValidateTokenResult(JwtSecurityToken token, ClaimsPrincipal principal)
                {
                    Token = token;
                    Principal = principal;
                }
            }
        }
    }
    
  • User Avatar
    0
    Pharm3r created

    Dear Support,

    1. Are there any working examples for Azure B2C with Angular? I am geting the same error as in https://support.aspnetzero.com/QA/Questions/10332/Azure-AD-B2C-login-implementation

    The only way I could get this to work is implement and call the above method "GetUserInfo" and change the line:

    From: var issuer = ProviderInfo.AdditionalParams["Authority"];

    To: var issuer = "https://<myTenantName>.b2clogin.com/<myTenantName>.onmicrosoft.com/<PolicyName>";

    Here are my appsettings.json for "OpenId":

    "IsEnabled": "true", "ClientId": "<AppRegistrationClientId>", "Authority": "https://<myTenantName>.b2clogin.com/<myTenantId>/v2.0/", "LoginUrl": "https://<myTenantName>.b2clogin.com/tfp/<myTenantName>.onmicrosoft.com/<PolicyName>/oauth2/v2.0/authorize",

    1. When using Azure B2C for a multi-tenant application, do I need to implement special logic so only users with a specific email alias can log into a tenant. For example, only users with email address @demo.com and login into tenant https://demo.myapp.com?

    Kind Regards,

    Darrell