问:
Authorize By Group in Azure Active Directory B2C
I am trying to figure out how to authorize using groups in Azure Active Directory B2C. I can Authorize via User, for example:

```<code>[Authorize(Users="Bill")]
</code>```

However, this is not very effective and I see very few use-cases for this. An alternate solution would be Authorizing via Role. However for some reason that does not seem to work. It does not work if I give a user the Role "Global Admin" for example, and try:

```<code>[Authorize(Roles="Global Admin")]
</code>```

Is there a way to authorize via Groups or Roles?

答:
I really like the answer from @AlexLobakov but I wanted an updated answer for .NET 6 and also something that was testable but still implemented the caching features. I also wanted the roles to be sent to my front end, be compatible with any SPA like React and use standard Azure AD B2C User flows for Role-based access control (RBAC) in my application.

I also missed a start to finish guide, so many variables that can go wrong and you end up with an application not working.

Start with creating a new ASP.NET Core Web API in Visual Studio 2022 with the following settings:

You should get a dialogue like this after creation:

If you don't see this then right click on the project in Visual Studio and click on Overview and then Connected services.

Create a new App registration in your Azure AD B2C or use an existing. I registered a new one for this demo purpose.

After creating the App registration Visual Studio got stuck on Dependency configuration progress so the rest will be configured manually:

Log on to https://portal.azure.com/, Switch directory to your AD B2C, select your new App registration and then click on Authentication. Then click on Add a platform and select Web.

Add a Redirect URI and Front-channel logout URL for localhost.

Example:

https://localhost:7166/signin-oidc

https://localhost:7166/logout

If you choose Single-page application instead it will look nearly the same. However you then need to add a code_challenge as described below. A full example for this will not be shown.

Is Active Directory not supporting Authorization Code Flow with PKCE?

Authentication should look something like this:

Click on Certificates & secrets and create a new Client secret.

Click on Expose an API and then edit Application ID URI.

Default value should look something like this api://11111111-1111-1111-1111-111111111111. Edit it to be https://youradb2c.onmicrosoft.com/11111111-1111-1111-1111-111111111111. There should be a scope named access_as_user. Create if it is not there.

Now click on API permissions:

Four Microsoft Graph permissions are needed.

Two Application:

```<code>GroupMember.Read.All
User.Read.All
</code>```

Two Delegated:

```<code>offline_access
openid
</code>```

You also need your access_as_user permission from My APIs. When this is done click on Grant admin consent for .... Should look like this:

If you don't have a User Flow already then create either a Sign up and sign in or a Sign in and select Recommended. My user flow is default B2C_1_signin.

Verify that your AD B2C user is a member of the group you want to authenticate against:

Now you can go back to your application and verify that you can get a code to login. Use this sample and it should redirect with a code:

```<code>https://<tenant-name>.b2clogin.com/tfp/<tenant-name>.onmicrosoft.com/<user-flow-name>/oauth2/v2.0/authorize?
client_id=<application-ID>
&nonce=anyRandomValue
&redirect_uri=https://localhost:7166/signin-oidc
&scope=https://<tenant-name>.onmicrosoft.com/11111111-1111-1111-1111-111111111111/access_as_user
&response_type=code
</code>```

If it works you should be redirected to something like this after login:

```<code>https://localhost:7166/signin-oidc?code=
</code>```

If you get an error that says:

AADB2C99059: The supplied request must present a code_challenge

Then you have probably selected platform Single-page application and needs to add a code_challenge to the request like: &code_challenge=123. This is not enough because you also need to validate the challenge later otherwise you will get the error below when running my code.

AADB2C90183: The supplied code_verifier is invalid

Now open your application and appsettings.json. Default should look something like this:

```<code>  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "qualified.domain.name",
    "TenantId": "22222222-2222-2222-2222-222222222222",
    "ClientId": "11111111-1111-1111-11111111111111111",

    "Scopes": "access_as_user",
    "CallbackPath": "/signin-oidc"
  },
</code>```

We need a few more values so it should look like this in the end:

```<code>  "AzureAd": {
    "Instance": "https://<tenant-name>.b2clogin.com/",
    "Domain": "<tenant-name>.onmicrosoft.com",
    "TenantId": "22222222-2222-2222-2222-222222222222",
    "ClientId": "11111111-1111-1111-11111111111111111",
    "SignUpSignInPolicyId": "B2C_1_signin",
    "ClientSecret": "--SECRET--",
    "ApiScope": "https://<tenant-name>.onmicrosoft.com/11111111-1111-1111-11111111111111111/access_as_user",
    "TokenUrl": "https://<tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/B2C_1_signin/oauth2/v2.0/token",
    "Scopes": "access_as_user",
    "CallbackPath": "/signin-oidc"
  },
</code>```

I store ClientSecret in Secret Manager.

https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows#manage-user-secrets-with-visual-studio

Now create these new classes:

AppSettings:

```<code>namespace AzureADB2CWebAPIGroupTest
{
    public class AppSettings
    {
        public AzureAdSettings AzureAd { get; set; } = new AzureAdSettings();

    }

    public class AzureAdSettings
    {
        public string Instance { get; set; }

        public string Domain { get; set; }

        public string TenantId { get; set; }

        public string ClientId { get; set; }

        public string IssuerSigningKey { get; set; }

        public string ValidIssuer { get; set; }

        public string ClientSecret { get; set; }

        public string ApiScope { get; set; }

        public string TokenUrl { get; set; }

    }
}
</code>```

Adb2cTokenResponse:

```<code>namespace AzureADB2CWebAPIGroupTest
{
    public class Adb2cTokenResponse
    {
        public string access_token { get; set; }
        public string id_token { get; set; }
        public string token_type { get; set; }
        public int not_before { get; set; }
        public int expires_in { get; set; }
        public int ext_expires_in { get; set; }
        public int expires_on { get; set; }
        public string resource { get; set; }
        public int id_token_expires_in { get; set; }
        public string profile_info { get; set; }
        public string scope { get; set; }
        public string refresh_token { get; set; }
        public int refresh_token_expires_in { get; set; }
    }
}
</code>```

CacheKeys:

```<code>namespace AzureADB2CWebAPIGroupTest
{
    public static class CacheKeys
    {
        public const string GraphApiAccessToken = "_GraphApiAccessToken";
    }
}
</code>```

GraphApiService:

```<code>using Microsoft.Extensions.Caching.Memory;
using Microsoft.Graph;
using System.Text.Json;

namespace AzureADB2CWebAPIGroupTest
{
    public class GraphApiService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly IMemoryCache _memoryCache;
        private readonly AppSettings _settings;
        private readonly string _accessToken;

        public GraphApiService(IHttpClientFactory clientFactory, IMemoryCache memoryCache, AppSettings settings)
        {
            _clientFactory = clientFactory;
            _memoryCache = memoryCache;
            _settings = settings;

            string graphApiAccessTokenCacheEntry;

            // Look for cache key.
            if (!_memoryCache.TryGetValue(CacheKeys.GraphApiAccessToken, out graphApiAccessTokenCacheEntry))
            {
                // Key not in cache, so get data.
                var adb2cTokenResponse = GetAccessTokenAsync().GetAwaiter().GetResult();

                graphApiAccessTokenCacheEntry = adb2cTokenResponse.access_token;

                // Set cache options.
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetAbsoluteExpiration(TimeSpan.FromSeconds(adb2cTokenResponse.expires_in));

                // Save data in cache.
                _memoryCache.Set(CacheKeys.GraphApiAccessToken, graphApiAccessTokenCacheEntry, cacheEntryOptions);
            }

            _accessToken = graphApiAccessTokenCacheEntry;
        }

        public async Task<List<string>> GetUserGroupsAsync(string oid)
        {
            var authProvider = new AuthenticationProvider(_accessToken);
            GraphServiceClient graphClient = new GraphServiceClient(authProvider, new HttpClientHttpProvider(_clientFactory.CreateClient()));

            //Requires GroupMember.Read.All and User.Read.All to get everything we want
            var groups = await graphClient.Users[oid].MemberOf.Request().GetAsync();

            if (groups == null)
            {
                return null;
            }

            var graphGroup = groups.Cast<Microsoft.Graph.Group>().ToList();

            return graphGroup.Select(x => x.DisplayName).ToList();
        }

        private async Task<Adb2cTokenResponse> GetAccessTokenAsync()
        {
            var client = _clientFactory.CreateClient();

            var kvpList = new List<KeyValuePair<string, string>>();
            kvpList.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
            kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAd.ClientId));
            kvpList.Add(new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"));
            kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAd.ClientSecret));

#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
            var req = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{_settings.AzureAd.Domain}/oauth2/v2.0/token")
            { Content = new FormUrlEncodedContent(kvpList) };
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation

            using var httpResponse = await client.SendAsync(req);

            var response = await httpResponse.Content.ReadAsStringAsync();

            httpResponse.EnsureSuccessStatusCode();

            var adb2cTokenResponse = JsonSerializer.Deserialize<Adb2cTokenResponse>(response);

            return adb2cTokenResponse;
        }
    }

    public class AuthenticationProvider : IAuthenticationProvider
    {
        private readonly string _accessToken;

        public AuthenticationProvider(string accessToken)
        {
            _accessToken = accessToken;
        }

        public Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            request.Headers.Add("Authorization", $"Bearer {_accessToken}");

            return Task.CompletedTask;
        }
    }

    public class HttpClientHttpProvider : IHttpProvider
    {
        private readonly HttpClient http;

        public HttpClientHttpProvider(HttpClient http)
        {
            this.http = http;
        }

        public ISerializer Serializer { get; } = new Serializer();

        public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

        public void Dispose()
        {
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            return http.SendAsync(request);
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
            return http.SendAsync(request, completionOption, cancellationToken);
        }
    }
}
</code>```

At the moment only accessToken for GraphServiceClient is stored in memorycache but if the application requires better performance a users groups could also be cached.

Add a new class:

Adb2cUser:

```<code>offline_access
openid
</code>```0

and struct:

```<code>offline_access
openid
</code>```1

And now add a new API Controller

LoginController:

```<code>offline_access
openid
</code>```2

Now it is time to edit Program.cs. Should look something like this for the new minimal hosting model in ASP.NET Core 6.0:

```<code>var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
</code>```

Notice that ASP.NET Core 6.0 are using JwtBearerDefaults.AuthenticationScheme and not AzureADB2CDefaults.AuthenticationScheme or AzureADB2CDefaults.OpenIdScheme.

Edit so Program.cs looks like this:

```<code>using AzureADB2CWebAPIGroupTest;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Identity.Web;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

//Used for debugging
//IdentityModelEventSource.ShowPII = true;

var settings = new AppSettings();
builder.Configuration.Bind(settings);
builder.Services.AddSingleton(settings);

var services = new ServiceCollection();
services.AddMemoryCache();
services.AddHttpClient();
var serviceProvider = services.BuildServiceProvider();

var memoryCache = serviceProvider.GetService<IMemoryCache>();
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();

var graphApiService = new GraphApiService(httpClientFactory, memoryCache, settings);

// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(options => {
        builder.Configuration.Bind("AzureAd", options);
        options.TokenValidationParameters.NameClaimType = "name";
        options.TokenValidationParameters.ValidateIssuerSigningKey = true;
        options.TokenValidationParameters.ValidateLifetime = true;
        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidateLifetime = true;
        options.TokenValidationParameters.ValidateTokenReplay = true;
        options.Audience = settings.AzureAd.ClientId;
        options.Events = new JwtBearerEvents()
        {
            OnTokenValidated = async ctx =>
            {
                //Runs on every request, cache a users groups if needed
                var oidClaim = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)ctx.SecurityToken).Claims.FirstOrDefault(c => c.Type == "oid");
                if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
                {
                    var groups = await graphApiService.GetUserGroupsAsync(oidClaim.Value);

                    foreach (var group in groups)
                    {
                        ((ClaimsIdentity)ctx.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role.ToString(), group));
                    }
                }
            }
        };
    },
    options => {
        builder.Configuration.Bind("AzureAd", options);
    });

builder.Services.AddTransient<GraphApiService>();

builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();
</code>```

Now you can run your application and use the code from earlier in a request like this:

```<code>POST /api/login/ HTTP/1.1
Host: localhost:7166
Content-Type: application/json

"code"
</code>```

You will then receieve a response like this with an access_token:

```<code>{
    "id": "31111111-1111-1111-1111-111111111111",
    "givenName": "Oscar",
    "familyName": "Andersson",
    "email": "oscar.andersson@example.com",
    "roles": [
        "Administrator",
    ],
    "adb2cTokenResponse": {

    }
}
</code>```

Adding [Authorize(Roles = "Administrator")] to WeatherForecastController.cs we can now verify that only a user with the correct role is allowed to access this resource using the access_token we got earlier:

If we change to [Authorize(Roles = "Administrator2")] we get a HTTP 403 with the same user:

LoginController can handle refresh tokens as well.

With NuGets Microsoft.NET.Test.Sdk, xunit, xunit.runner.visualstudio and Moq we can also test LoginController and in turn also GraphApiService used for ClaimsIdentity in Program.cs. Unfortunately due body being limited to 30000 charcters the entire test can not be shown.

It basically looks like this:

LoginControllerTest:

```<code>using AzureADB2CWebAPIGroupTest.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using Moq.Protected;
using System.Net;
using Xunit;

namespace AzureADB2CWebAPIGroupTest
{
    public class LoginControllerTest
    {

        [Theory]
        [MemberData(nameof(PostData))]
        public async Task Post(string code, string response, string expectedEmail, string expectedFamilyName, string expectedGivenName)
        {
            var controller = GetLoginController(response);

            var result = await controller.Post(code);

            var actionResult = Assert.IsType<ActionResult<Adb2cUser>>(result);
            var okResult = Assert.IsType<OkObjectResult>(result.Result);
            var returnValue = Assert.IsType<Adb2cUser>(okResult.Value);
            Assert.Equal(returnValue.Email, expectedEmail);
            Assert.Equal(returnValue.Roles[1], GraphApiServiceMock.DummyGroup2Name);
        }

        [Theory]
        [MemberData(nameof(RefreshData))]
        public async Task Refresh(string code, string response, string expectedEmail, string expectedFamilyName, string expectedGivenName)
        {
            var controller = GetLoginController(response);

            var result = await controller.Refresh(code);

            var actionResult = Assert.IsType<ActionResult<Adb2cUser>>(result);
            var okResult = Assert.IsType<OkObjectResult>(result.Result);
            var returnValue = Assert.IsType<Adb2cUser>(okResult.Value);
            Assert.Equal(returnValue.Email, expectedEmail);
            Assert.Equal(returnValue.Roles[1], GraphApiServiceMock.DummyGroup2Name);
        }

        //PostData and RefreshData removed for space

        private LoginController GetLoginController(string expectedResponse)
        {
            var mockFactory = new Mock<IHttpClientFactory>();

            var settings = new AppSettings();

            settings.AzureAd.TokenUrl = "https://example.com";

            var mockMessageHandler = new Mock<HttpMessageHandler>();

            GraphApiServiceMock.MockHttpRequests(mockMessageHandler);

            mockMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains(settings.AzureAd.TokenUrl)), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(expectedResponse)
                });

            var httpClient = new HttpClient(mockMessageHandler.Object);

            mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);

            var logger = Mock.Of<ILogger<LoginController>>();

            var services = new ServiceCollection();
            services.AddMemoryCache();
            var serviceProvider = services.BuildServiceProvider();

            var memoryCache = serviceProvider.GetService<IMemoryCache>();

            var graphService = new GraphApiService(mockFactory.Object, memoryCache, settings);

            var controller = new LoginController(logger, mockFactory.Object, settings, graphService);

            return controller;
        }
    }
}
</code>```

A GraphApiServiceMock.cs is also needed but it just adds more values like the example with mockMessageHandler.Protected() and static values like public static string DummyUserExternalId = "11111111-1111-1111-1111-111111111111";.

There are other ways to do this but they usually depend on Custom Policies:

https://learn.microsoft.com/en-us/answers/questions/469509/can-we-get-and-edit-azure-ad-b2c-roles-using-ad-b2.html

https://devblogs.microsoft.com/premier-developer/using-groups-in-azure-ad-b2c/

https://learn.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview

Body:
I am trying to figure out how to authorize using groups in Azure Active Directory B2C. I can Authorize via User, for example:

[Authorize(Users="Bill")]

However, this is not very effective and I see very few use-cases for this. An alternate solution would be Authorizing via Role. However for some reason that does not seem to work. It does not work if I give a user the Role "Global Admin" for example, and try:

[Authorize(Roles="Global Admin")]

Is there a way to authorize via Groups or Roles?

Body:
I really like the answer from @AlexLobakov but I wanted an updated answer for .NET 6 and also something that was testable but still implemented the caching features. I also wanted the roles to be sent to my front end, be compatible with any SPA like React and use standard Azure AD B2C User flows for Role-based access control (RBAC) in my application.

I also missed a start to finish guide, so many variables that can go wrong and you end up with an application not working.

Start with creating a new ASP.NET Core Web API in Visual Studio 2022 with the following settings:

You should get a dialogue like this after creation:

If you don't see this then right click on the project in Visual Studio and click on Overview and then Connected services.

Create a new App registration in your Azure AD B2C or use an existing. I registered a new one for this demo purpose.

After creating the App registration Visual Studio got stuck on Dependency configuration progress so the rest will be configured manually:

Log on to https://portal.azure.com/, Switch directory to your AD B2C, select your new App registration and then click on Authentication. Then click on Add a platform and select Web.

Add a Redirect URI and Front-channel logout URL for localhost.

Example:

https://localhost:7166/signin-oidc

https://localhost:7166/logout

If you choose Single-page application instead it will look nearly the same. However you then need to add a code_challenge as described below. A full example for this will not be shown.

Is Active Directory not supporting Authorization Code Flow with PKCE?

Authentication should look something like this:

Click on Certificates & secrets and create a new Client secret.

Click on Expose an API and then edit Application ID URI.

Default value should look something like this api://11111111-1111-1111-1111-111111111111. Edit it to be https://youradb2c.onmicrosoft.com/11111111-1111-1111-1111-111111111111. There should be a scope named access_as_user. Create if it is not there.

Now click on API permissions:

Four Microsoft Graph permissions are needed.

Two Application:

GroupMember.Read.All
User.Read.All

Two Delegated:

offline_access
openid

You also need your access_as_user permission from My APIs. When this is done click on Grant admin consent for .... Should look like this:

If you don't have a User Flow already then create either a Sign up and sign in or a Sign in and select Recommended. My user flow is default B2C_1_signin.

Verify that your AD B2C user is a member of the group you want to authenticate against:

Now you can go back to your application and verify that you can get a code to login. Use this sample and it should redirect with a code:

https://<tenant-name>.b2clogin.com/tfp/<tenant-name>.onmicrosoft.com/<user-flow-name>/oauth2/v2.0/authorize?
client_id=<application-ID>
&nonce=anyRandomValue
&redirect_uri=https://localhost:7166/signin-oidc
&scope=https://<tenant-name>.onmicrosoft.com/11111111-1111-1111-1111-111111111111/access_as_user
&response_type=code

If it works you should be redirected to something like this after login:

https://localhost:7166/signin-oidc?code=

If you get an error that says:

AADB2C99059: The supplied request must present a code_challenge

Then you have probably selected platform Single-page application and needs to add a code_challenge to the request like: &code_challenge=123. This is not enough because you also need to validate the challenge later otherwise you will get the error below when running my code.

AADB2C90183: The supplied code_verifier is invalid

Now open your application and appsettings.json. Default should look something like this:

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "qualified.domain.name",
    "TenantId": "22222222-2222-2222-2222-222222222222",
    "ClientId": "11111111-1111-1111-11111111111111111",

    "Scopes": "access_as_user",
    "CallbackPath": "/signin-oidc"
  },

We need a few more values so it should look like this in the end:

  "AzureAd": {
    "Instance": "https://<tenant-name>.b2clogin.com/",
    "Domain": "<tenant-name>.onmicrosoft.com",
    "TenantId": "22222222-2222-2222-2222-222222222222",
    "ClientId": "11111111-1111-1111-11111111111111111",
    "SignUpSignInPolicyId": "B2C_1_signin",
    "ClientSecret": "--SECRET--",
    "ApiScope": "https://<tenant-name>.onmicrosoft.com/11111111-1111-1111-11111111111111111/access_as_user",
    "TokenUrl": "https://<tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/B2C_1_signin/oauth2/v2.0/token",
    "Scopes": "access_as_user",
    "CallbackPath": "/signin-oidc"
  },

I store ClientSecret in Secret Manager.

https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows#manage-user-secrets-with-visual-studio

Now create these new classes:

AppSettings:

namespace AzureADB2CWebAPIGroupTest
{
    public class AppSettings
    {
        public AzureAdSettings AzureAd { get; set; } = new AzureAdSettings();

    }

    public class AzureAdSettings
    {
        public string Instance { get; set; }

        public string Domain { get; set; }

        public string TenantId { get; set; }

        public string ClientId { get; set; }

        public string IssuerSigningKey { get; set; }

        public string ValidIssuer { get; set; }

        public string ClientSecret { get; set; }

        public string ApiScope { get; set; }

        public string TokenUrl { get; set; }

    }
}

Adb2cTokenResponse:

namespace AzureADB2CWebAPIGroupTest
{
    public class Adb2cTokenResponse
    {
        public string access_token { get; set; }
        public string id_token { get; set; }
        public string token_type { get; set; }
        public int not_before { get; set; }
        public int expires_in { get; set; }
        public int ext_expires_in { get; set; }
        public int expires_on { get; set; }
        public string resource { get; set; }
        public int id_token_expires_in { get; set; }
        public string profile_info { get; set; }
        public string scope { get; set; }
        public string refresh_token { get; set; }
        public int refresh_token_expires_in { get; set; }
    }
}

CacheKeys:

namespace AzureADB2CWebAPIGroupTest
{
    public static class CacheKeys
    {
        public const string GraphApiAccessToken = "_GraphApiAccessToken";
    }
}

GraphApiService:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Graph;
using System.Text.Json;

namespace AzureADB2CWebAPIGroupTest
{
    public class GraphApiService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly IMemoryCache _memoryCache;
        private readonly AppSettings _settings;
        private readonly string _accessToken;

        public GraphApiService(IHttpClientFactory clientFactory, IMemoryCache memoryCache, AppSettings settings)
        {
            _clientFactory = clientFactory;
            _memoryCache = memoryCache;
            _settings = settings;

            string graphApiAccessTokenCacheEntry;

            // Look for cache key.
            if (!_memoryCache.TryGetValue(CacheKeys.GraphApiAccessToken, out graphApiAccessTokenCacheEntry))
            {
                // Key not in cache, so get data.
                var adb2cTokenResponse = GetAccessTokenAsync().GetAwaiter().GetResult();

                graphApiAccessTokenCacheEntry = adb2cTokenResponse.access_token;

                // Set cache options.
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetAbsoluteExpiration(TimeSpan.FromSeconds(adb2cTokenResponse.expires_in));

                // Save data in cache.
                _memoryCache.Set(CacheKeys.GraphApiAccessToken, graphApiAccessTokenCacheEntry, cacheEntryOptions);
            }

            _accessToken = graphApiAccessTokenCacheEntry;
        }

        public async Task<List<string>> GetUserGroupsAsync(string oid)
        {
            var authProvider = new AuthenticationProvider(_accessToken);
            GraphServiceClient graphClient = new GraphServiceClient(authProvider, new HttpClientHttpProvider(_clientFactory.CreateClient()));

            //Requires GroupMember.Read.All and User.Read.All to get everything we want
            var groups = await graphClient.Users[oid].MemberOf.Request().GetAsync();

            if (groups == null)
            {
                return null;
            }

            var graphGroup = groups.Cast<Microsoft.Graph.Group>().ToList();

            return graphGroup.Select(x => x.DisplayName).ToList();
        }

        private async Task<Adb2cTokenResponse> GetAccessTokenAsync()
        {
            var client = _clientFactory.CreateClient();

            var kvpList = new List<KeyValuePair<string, string>>();
            kvpList.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
            kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAd.ClientId));
            kvpList.Add(new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"));
            kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAd.ClientSecret));

#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
            var req = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{_settings.AzureAd.Domain}/oauth2/v2.0/token")
            { Content = new FormUrlEncodedContent(kvpList) };
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation

            using var httpResponse = await client.SendAsync(req);

            var response = await httpResponse.Content.ReadAsStringAsync();

            httpResponse.EnsureSuccessStatusCode();

            var adb2cTokenResponse = JsonSerializer.Deserialize<Adb2cTokenResponse>(response);

            return adb2cTokenResponse;
        }
    }

    public class AuthenticationProvider : IAuthenticationProvider
    {
        private readonly string _accessToken;

        public AuthenticationProvider(string accessToken)
        {
            _accessToken = accessToken;
        }

        public Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            request.Headers.Add("Authorization", $"Bearer {_accessToken}");

            return Task.CompletedTask;
        }
    }

    public class HttpClientHttpProvider : IHttpProvider
    {
        private readonly HttpClient http;

        public HttpClientHttpProvider(HttpClient http)
        {
            this.http = http;
        }

        public ISerializer Serializer { get; } = new Serializer();

        public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

        public void Dispose()
        {
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            return http.SendAsync(request);
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
            return http.SendAsync(request, completionOption, cancellationToken);
        }
    }
}

At the moment only accessToken for GraphServiceClient is stored in memorycache but if the application requires better performance a users groups could also be cached.

Add a new class:

Adb2cUser:

namespace AzureADB2CWebAPIGroupTest
{
    public class Adb2cUser
    {
        public Guid Id { get; set; }

        public string GivenName { get; set; }

        public string FamilyName { get; set; }

        public string Email { get; set; }

        public List<string> Roles { get; set; }

        public Adb2cTokenResponse Adb2cTokenResponse { get; set; }
    }
}

and struct:

namespace AzureADB2CWebAPIGroupTest
{
    public struct ADB2CJwtRegisteredClaimNames
    {
        public const string Emails = "emails";

        public const string Name = "name";
    }
}

And now add a new API Controller

LoginController:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;

namespace AzureADB2CWebAPIGroupTest.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class LoginController : ControllerBase
    {

        private readonly ILogger<LoginController> _logger;
        private readonly IHttpClientFactory _clientFactory;
        private readonly AppSettings _settings;
        private readonly GraphApiService _graphApiService;

        public LoginController(ILogger<LoginController> logger, IHttpClientFactory clientFactory, AppSettings settings, GraphApiService graphApiService)
        {
            _logger = logger;
            _clientFactory = clientFactory;
            _settings = settings;
            _graphApiService=graphApiService;
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<ActionResult<Adb2cUser>> Post([FromBody] string code)
        {
            var redirectUri = "";

            if (HttpContext != null)
            {
                redirectUri = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host + "/signin-oidc";
            }

            var kvpList = new List<KeyValuePair<string, string>>();
            kvpList.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
            kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAd.ClientId));
            kvpList.Add(new KeyValuePair<string, string>("scope", "openid offline_access " + _settings.AzureAd.ApiScope));
            kvpList.Add(new KeyValuePair<string, string>("code", code));
            kvpList.Add(new KeyValuePair<string, string>("redirect_uri", redirectUri));
            kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAd.ClientSecret));

            return await UserLoginAndRefresh(kvpList);

        }

        [HttpPost("refresh")]
        [AllowAnonymous]
        public async Task<ActionResult<Adb2cUser>> Refresh([FromBody] string token)
        {
            var redirectUri = "";

            if (HttpContext != null)
            {
                redirectUri = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host;
            }

            var kvpList = new List<KeyValuePair<string, string>>();
            kvpList.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
            kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAd.ClientId));
            kvpList.Add(new KeyValuePair<string, string>("scope", "openid offline_access " + _settings.AzureAd.ApiScope));
            kvpList.Add(new KeyValuePair<string, string>("refresh_token", token));
            kvpList.Add(new KeyValuePair<string, string>("redirect_uri", redirectUri));
            kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAd.ClientSecret));

            return await UserLoginAndRefresh(kvpList);
        }

        private async Task<ActionResult<Adb2cUser>> UserLoginAndRefresh(List<KeyValuePair<string, string>> kvpList)
        {
            var user = await TokenRequest(kvpList);
            if (user == null)
            {
                return Unauthorized();
            }

            //Return access token and user information
            return Ok(user);
        }

        private async Task<Adb2cUser> TokenRequest(List<KeyValuePair<string, string>> keyValuePairs)
        {
            var client = _clientFactory.CreateClient();

#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
            var req = new HttpRequestMessage(HttpMethod.Post, _settings.AzureAd.TokenUrl)
            { Content = new FormUrlEncodedContent(keyValuePairs) };
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation

            using var httpResponse = await client.SendAsync(req);

            var response = await httpResponse.Content.ReadAsStringAsync();

            httpResponse.EnsureSuccessStatusCode();

            var adb2cTokenResponse = JsonSerializer.Deserialize<Adb2cTokenResponse>(response);

            var handler = new JwtSecurityTokenHandler();
            var jwtSecurityToken = handler.ReadJwtToken(adb2cTokenResponse.access_token);

            var id = jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.Sub).Value;

            var groups = await _graphApiService.GetUserGroupsAsync(id);

            var givenName = jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.GivenName).Value;
            var familyName = jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.FamilyName).Value;
            //Unless Alternate email have been added in Azure AD there will only be one email here.
            //TODO Handle multiple emails
            var emails = jwtSecurityToken.Claims.First(claim => claim.Type == ADB2CJwtRegisteredClaimNames.Emails).Value;

            var user = new Adb2cUser()
            {
                Id = Guid.Parse(id),
                GivenName = givenName,
                FamilyName = familyName,
                Email = emails,
                Roles = groups,
                Adb2cTokenResponse = adb2cTokenResponse
            };

            return user;
        }
    }
}

Now it is time to edit Program.cs. Should look something like this for the new minimal hosting model in ASP.NET Core 6.0:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

Notice that ASP.NET Core 6.0 are using JwtBearerDefaults.AuthenticationScheme and not AzureADB2CDefaults.AuthenticationScheme or AzureADB2CDefaults.OpenIdScheme.

Edit so Program.cs looks like this:

using AzureADB2CWebAPIGroupTest;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Identity.Web;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

//Used for debugging
//IdentityModelEventSource.ShowPII = true;

var settings = new AppSettings();
builder.Configuration.Bind(settings);
builder.Services.AddSingleton(settings);

var services = new ServiceCollection();
services.AddMemoryCache();
services.AddHttpClient();
var serviceProvider = services.BuildServiceProvider();

var memoryCache = serviceProvider.GetService<IMemoryCache>();
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();

var graphApiService = new GraphApiService(httpClientFactory, memoryCache, settings);

// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(options => {
        builder.Configuration.Bind("AzureAd", options);
        options.TokenValidationParameters.NameClaimType = "name";
        options.TokenValidationParameters.ValidateIssuerSigningKey = true;
        options.TokenValidationParameters.ValidateLifetime = true;
        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidateLifetime = true;
        options.TokenValidationParameters.ValidateTokenReplay = true;
        options.Audience = settings.AzureAd.ClientId;
        options.Events = new JwtBearerEvents()
        {
            OnTokenValidated = async ctx =>
            {
                //Runs on every request, cache a users groups if needed
                var oidClaim = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)ctx.SecurityToken).Claims.FirstOrDefault(c => c.Type == "oid");
                if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
                {
                    var groups = await graphApiService.GetUserGroupsAsync(oidClaim.Value);

                    foreach (var group in groups)
                    {
                        ((ClaimsIdentity)ctx.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role.ToString(), group));
                    }
                }
            }
        };
    },
    options => {
        builder.Configuration.Bind("AzureAd", options);
    });

builder.Services.AddTransient<GraphApiService>();

builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Now you can run your application and use the code from earlier in a request like this:

POST /api/login/ HTTP/1.1
Host: localhost:7166
Content-Type: application/json

"code"

You will then receieve a response like this with an access_token:

{
    "id": "31111111-1111-1111-1111-111111111111",
    "givenName": "Oscar",
    "familyName": "Andersson",
    "email": "oscar.andersson@example.com",
    "roles": [
        "Administrator",
    ],
    "adb2cTokenResponse": {

    }
}

Adding [Authorize(Roles = "Administrator")] to WeatherForecastController.cs we can now verify that only a user with the correct role is allowed to access this resource using the access_token we got earlier:

If we change to [Authorize(Roles = "Administrator2")] we get a HTTP 403 with the same user:

LoginController can handle refresh tokens as well.

With NuGets Microsoft.NET.Test.Sdk, xunit, xunit.runner.visualstudio and Moq we can also test LoginController and in turn also GraphApiService used for ClaimsIdentity in Program.cs. Unfortunately due body being limited to 30000 charcters the entire test can not be shown.

It basically looks like this:

LoginControllerTest:

using AzureADB2CWebAPIGroupTest.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using Moq.Protected;
using System.Net;
using Xunit;

namespace AzureADB2CWebAPIGroupTest
{
    public class LoginControllerTest
    {

        [Theory]
        [MemberData(nameof(PostData))]
        public async Task Post(string code, string response, string expectedEmail, string expectedFamilyName, string expectedGivenName)
        {
            var controller = GetLoginController(response);

            var result = await controller.Post(code);

            var actionResult = Assert.IsType<ActionResult<Adb2cUser>>(result);
            var okResult = Assert.IsType<OkObjectResult>(result.Result);
            var returnValue = Assert.IsType<Adb2cUser>(okResult.Value);
            Assert.Equal(returnValue.Email, expectedEmail);
            Assert.Equal(returnValue.Roles[1], GraphApiServiceMock.DummyGroup2Name);
        }

        [Theory]
        [MemberData(nameof(RefreshData))]
        public async Task Refresh(string code, string response, string expectedEmail, string expectedFamilyName, string expectedGivenName)
        {
            var controller = GetLoginController(response);

            var result = await controller.Refresh(code);

            var actionResult = Assert.IsType<ActionResult<Adb2cUser>>(result);
            var okResult = Assert.IsType<OkObjectResult>(result.Result);
            var returnValue = Assert.IsType<Adb2cUser>(okResult.Value);
            Assert.Equal(returnValue.Email, expectedEmail);
            Assert.Equal(returnValue.Roles[1], GraphApiServiceMock.DummyGroup2Name);
        }

        //PostData and RefreshData removed for space

        private LoginController GetLoginController(string expectedResponse)
        {
            var mockFactory = new Mock<IHttpClientFactory>();

            var settings = new AppSettings();

            settings.AzureAd.TokenUrl = "https://example.com";

            var mockMessageHandler = new Mock<HttpMessageHandler>();

            GraphApiServiceMock.MockHttpRequests(mockMessageHandler);

            mockMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains(settings.AzureAd.TokenUrl)), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(expectedResponse)
                });

            var httpClient = new HttpClient(mockMessageHandler.Object);

            mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);

            var logger = Mock.Of<ILogger<LoginController>>();

            var services = new ServiceCollection();
            services.AddMemoryCache();
            var serviceProvider = services.BuildServiceProvider();

            var memoryCache = serviceProvider.GetService<IMemoryCache>();

            var graphService = new GraphApiService(mockFactory.Object, memoryCache, settings);

            var controller = new LoginController(logger, mockFactory.Object, settings, graphService);

            return controller;
        }
    }
}

A GraphApiServiceMock.cs is also needed but it just adds more values like the example with mockMessageHandler.Protected() and static values like public static string DummyUserExternalId = "11111111-1111-1111-1111-111111111111";.

There are other ways to do this but they usually depend on Custom Policies:

https://learn.microsoft.com/en-us/answers/questions/469509/can-we-get-and-edit-azure-ad-b2c-roles-using-ad-b2.html

https://devblogs.microsoft.com/premier-developer/using-groups-in-azure-ad-b2c/

https://learn.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview


来源: stackexchange

问:
Mozilla doesn't refresh the audio tag file. Mozilla's faulty audio
I have a code that auto-generates captcha image by calling a servlet and displays it in an HTML form. But for the visually impaired I wanted to put an audio file as well. So I store the captcha code generated by the servlet as a session attribute in String form and from that attribute again generate an audio file according to the captcha. So lets say if captcha is "c-a-t" the audio file is generated accordingly. Now the problem is when I refresh the page in Chrome the captcha image and audio both get refreshed which is ideal but in Mozilla only the image is getting refreshed not the audio file.</p>

```<code>    <div class="formRow">
                <div class="field">
                    <img style="margin-left:91px; margin-top:-6px;" class="image" src="http:www.xyz.com/captchaServlet">
         //the above line calls a servlet for the image

                </div>
                </div></br>
                 <div class="formRow">
                      <div class="field">
                      <audio style="margin-left:91px;" controls="controls">
      <source  src="http:www.xyz.com/getSound" type="audio/wav">
//the above line calls servlet for audio file
    Your browser does not support this audio format.
    </audio>
                      </div>
             </div>
</code>```

答:
This is an interesting problem and a lot of learning can be gained by solving this bug.

To start with, Mozilla Firefox was unable to figure out the difference between the URL "http:abcd.com/getSound" clicked at different instances. For example, the click at 9:30 AM and the click at 9:31 AM wouldn't have any difference for Firefox because the same URL was requested at both instances.

One simple way to solve this issue is to include a unique parameter at the end of the URL to make sure that Firefox is able to differentiate between web requests.

```

Adding this on load JS function will change the src URL after each time the page is loaded and since a different URL is called each time, the browser won't find the new URL in its cache and will retrieve the correct audio file for each corresponding click.

The solution is 99% similar to @Nick Div although my curiosity made me dig into more details. I'm still curious to know why this issue specifically occurs in Firefox and not for other browsers. Will return once I find out the answer.

ContentLicense: CC BY-SA 3.0

Body:
I have a code that auto-generates captcha image by calling a servlet and displays it in an HTML form. But for the visually impaired I wanted to put an audio file as well. So I store the captcha code generated by the servlet as a session attribute in String form and from that attribute again generate an audio file according to the captcha. So lets say if captcha is "c-a-t" the audio file is generated accordingly. Now the problem is when I refresh the page in Chrome the captcha image and audio both get refreshed which is ideal but in Mozilla only the image is getting refreshed not the audio file.

         //the above line calls a servlet for the image

                      <audio style="margin-left:91px;" controls="controls">
      <source  src="http:www.xyz.com/getSound" type="audio/wav">
//the above line calls servlet for audio file
    Your browser does not support this audio format.
    </audio>

Body:
This is an interesting problem and a lot of learning can be gained by solving this bug.

To start with, Mozilla Firefox was unable to figure out the difference between the URL "http:abcd.com/getSound" clicked at different instances. For example, the click at 9:30 AM and the click at 9:31 AM wouldn't have any difference for Firefox because the same URL was requested at both instances.

One simple way to solve this issue is to include a unique parameter at the end of the URL to make sure that Firefox is able to differentiate between web requests.

<script type="text/javascript">
  window.onload = (event) => {
    var d = new Date();
    var n = d.getTime();
    document.getElementById("audiowav").src="http://abcd.com/getSound?rand="+n;
    };
</script>

Adding this on load JS function will change the src URL after each time the page is loaded and since a different URL is called each time, the browser won't find the new URL in its cache and will retrieve the correct audio file for each corresponding click.

The solution is 99% similar to @Nick Div although my curiosity made me dig into more details. I'm still curious to know why this issue specifically occurs in Firefox and not for other browsers. Will return once I find out the answer.

ContentLicense: CC BY-SA 4.0


问:
Weird line on radio buttons when displayed on a iPhone
I've made a donation form with radio buttons. It's fully styled with CSS.
When i open it on a Windows computer / android phone / iMac, everything looks fine.
But when i open it on a iPhone i get some strange lines where the radio button is.
The lines have a weird position, but that's because of the padding.

Does anybody know how i can get rid of the lines on the iPhone?

Thanks in advance!

答:
That is caused by the original radio button's border. You can eliminate it with

input[type="radio"] {
    border: none;
}
```


问:
Insert to web-page one instance of custom HTML element more than once
As example, I dynamically create instance of some custom HTML element.
After this, I twice use document.body.appendChild() and this work, but this should no be. What I may do for prevent this?

I put sample about this situation below in the text. RandomParagraphSizePlaceholder is simple web-component from some book. I use it only for illustration purposes.

https://jsfiddle.net/h6cxjae5/

P.S. I see, that is something wrong with web component, may be.

答:
You get two instances, because your code:

 this.appendChild(document.importNode(placeholder.content, true));
```

effectively makes a copy (importNode) and then appends another element.

connectedCallback (corretcly running twice) has (in this case) nothing to do with it.

https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode

Body:
You get two instances, because your code:

 this.appendChild(document.importNode(placeholder.content, true));

effectively makes a copy (importNode) and then appends another element.

connectedCallback (corretcly running twice) has (in this case) nothing to do with it.

https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode


问:
Cannot open redirect link in facebook iphone app
I have a problem with the facebook iphone app. I post a link in the wall in my facebook account and I used my iphone 4 to check it. The link I posted redirects to another page. I started the app and saw my wall and when I clicked the link, it crashes and put me out of facebook and not Safari is opened.
This thing did not happen before and now it happens only with links that has redirection.

Does anyone have an idea of what could be the problem?

thanks

答:
maybe the application has crashed.
if you dont try reinstall firstly remove and reinstall application in iTunes.
when you've reinstall please sign in another account and try redirect link.
check safari's permission setting.

or

use safari and log in facebook mobile. below the page the was a link like download for iphone. download this and try login with app.

I hope you solved the problem


问:
Automatic select only member in the object list
Is there a way to automatically address/select the only member in a object?
In my case the 3rd level member jobf can vary. But on that level it will always only have one member.

```<code>### get aeObject ###
try {
    $response = Invoke-RestMethod -Method GET -Uri "$client_id/objects/$($i -replace "#","%23")" -Headers $header
}
catch {
    Write-Error -Message "$($_.Exception.Message)"
    $Error[0] | Format-List -Force
}

### modify JSON and import it ###
$JSON = $response | ConvertTo-Json -Depth 100 | ConvertFrom-Json
$JSON.data
if ($JSON.data.jobf.general_attributes.time_zone) {
    $JSON.data.jobf.general_attributes.time_zone = "$TZ"
}
else {
    $JSON.data.jobf.general_attributes  | Add-Member -Name "time_zone" -MemberType NoteProperty -Value "$TZ"
}
</code>```

enter image description here

答:
You can use Get-Member to iterate the members at the third level, then filter out the noise and select the name of the first (and only one) member.

```<code># Get the third level single item (eg: jobf, jobx, etc)
$Job = ($JSON.data | Get-Member -MemberType NoteProperty | Select-Object -First 1).Name

# Instead of "jobf", we use the value stored in $Job
if ($JSON.data.$Job.general_attributes.time_zone) {
    $JSON.data.$Job.general_attributes.time_zone = "$TZ"
}
else {
    $JSON.data.$Job.general_attributes  | Add-Member -Name "time_zone" -MemberType NoteProperty -Value "$TZ"
}
</code>```

References

Get-Member - Gets the properties and methods of objects.

Body:
Is there a way to automatically address/select the only member in a object?
In my case the 3rd level member jobf can vary. But on that level it will always only have one member.

### get aeObject ###
try {
    $response = Invoke-RestMethod -Method GET -Uri "$client_id/objects/$($i -replace "#","%23")" -Headers $header
}
catch {
    Write-Error -Message "$($_.Exception.Message)"
    $Error[0] | Format-List -Force
}

### modify JSON and import it ###
$JSON = $response | ConvertTo-Json -Depth 100 | ConvertFrom-Json
$JSON.data
if ($JSON.data.jobf.general_attributes.time_zone) {
    $JSON.data.jobf.general_attributes.time_zone = "$TZ"
}
else {
    $JSON.data.jobf.general_attributes  | Add-Member -Name "time_zone" -MemberType NoteProperty -Value "$TZ"
}

enter image description here

Body:
You can use Get-Member to iterate the members at the third level, then filter out the noise and select the name of the first (and only one) member.

# Get the third level single item (eg: jobf, jobx, etc)
$Job = ($JSON.data | Get-Member -MemberType NoteProperty | Select-Object -First 1).Name

# Instead of "jobf", we use the value stored in $Job
if ($JSON.data.$Job.general_attributes.time_zone) {
    $JSON.data.$Job.general_attributes.time_zone = "$TZ"
}
else {
    $JSON.data.$Job.general_attributes  | Add-Member -Name "time_zone" -MemberType NoteProperty -Value "$TZ"
}

References

Get-Member - Gets the properties and methods of objects.


问:
What is the limit number of GitLab CI/CD variables?
I am planning to store some of the key/values inside GitLab CI/CD variables? I found in the document that each value is limited to 1000 characters. I am wondering if there is any limitation for the number of keys/values as well?</p>

答:
For the default plan the limit of variables in self hosted instance would be 25 according to https://docs.gitlab.com/ee/administration/instance_limits.html#number-of-instance-level-variables

But it states that you can update this number


问:
Unpacking nested data from Rapid API
pretty new to both stack overflow and python so my apologies if the formatting is wrong. Not sure if necessary but I am using VS Code.

I am pulling data from a covid-19 rapid api and it outputs the data (json) but doesn't give me the data I actually need:

This is the code to pull it:

```<code>import requests

url = "https://covid-19-data.p.rapidapi.com/report/country/code"

querystring = {"date":"2020-04-01","code":"it"}
headers = { 'x-rapidapi-host': "covid-19-data.p.rapidapi.com", 'x-rapidapi-key': "SIGN-UP-FOR-KEY" }

response = requests.request("GET", url, headers=headers, params=querystring) print(response.text)
</code>```

Once I get that, it outputs:

```<code>[{"country":"USA","provinces":[{"province":"USA"}],"latitude":37.09024,"longitude":-95.712891,"date":"2022-01-24"}]
</code>```

However, on the rapid api website, it shows under 'Example Responses' that the data I need (deaths, recovered, active, etc.) is nested under the  [{"province":"USA"}]  section:

```<code>[1 item
    0:{5 items
         "country":"Italy"
                 "provinces":[1 item
                    0:{5 items
                           "province":"Italy"
                           "confirmed":110574
                           "recovered":16847
                           "deaths":13155
                           "active":80572
                        }
                     ]
             "latitude":41.87194
             "longitude":12.56738
             "date":"2020-04-01"
        }
    ]
</code>```

I want to pull the data from that nested dictionary but I am not sure how.
This is my attempt which hasn't worked but just wanted to show the data:

```<code>import requests
import json

def get_covid_data_19(country_code, date):
    '''
    This function gets COVID 19 data via rapid API. This takes two parameters:
    Country Name (string) and Date (YYYY-MM-DD)

    '''
    c = country_code
    d = date
    # Endpoint that we are going to be hitting:
    url = "https://covid-19-data.p.rapidapi.com/report/country/code"

    querystring = {"code" : c, "date" : d}

    headers = {
        'x-rapidapi-host': "covid-19-data.p.rapidapi.com",
        'x-rapidapi-key': "insertkeyhere"
        }

    response = requests.request("GET", url, headers=headers, params=querystring)
    json_data = response.json()

    data = []
    for response in json_data:
        data.append ({
            'confirmed' : response.get('confirmed'),
            'recovered' : response.get('recovered'),
            'deaths' : response.get('deaths'),
            'active' : response.get('active'),
            'date' : response.get('date'),
        })

    print(data)
</code>```

I also attemped to retrieve the data as xml format and could not figure out how to expand the array either.
Any advice would be awesome. Thanks.

答:
These are all the endpoints of the API.

You are using the "Country/Code" endpoint, which will return the Covid stats in the provinces array. You need to traverse the array to get the stats of each province in the country. I have confirmed that it shows stats for USA. Try looking at this official RapidAPI guide on creating an app with this API, it might help.

Title: Unpacking nested data from Rapid API

Body:
pretty new to both stack overflow and python so my apologies if the formatting is wrong. Not sure if necessary but I am using VS Code.

I am pulling data from a covid-19 rapid api and it outputs the data (json) but doesn't give me the data I actually need:

This is the code to pull it:

import requests

url = "https://covid-19-data.p.rapidapi.com/report/country/code"

querystring = {"date":"2020-04-01","code":"it"}
headers = { 'x-rapidapi-host': "covid-19-data.p.rapidapi.com", 'x-rapidapi-key': "SIGN-UP-FOR-KEY" }

response = requests.request("GET", url, headers=headers, params=querystring) print(response.text)

Once I get that, it outputs:

[{"country":"USA","provinces":[{"province":"USA"}],"latitude":37.09024,"longitude":-95.712891,"date":"2022-01-24"}]

However, on the rapid api website, it shows under 'Example Responses' that the data I need (deaths, recovered, active, etc.) is nested under the  [{"province":"USA"}]  section:

[1 item
    0:{5 items
         "country":"Italy"
                 "provinces":[1 item
                    0:{5 items
                           "province":"Italy"
                           "confirmed":110574
                           "recovered":16847
                           "deaths":13155
                           "active":80572
                        }
                     ]
             "latitude":41.87194
             "longitude":12.56738
             "date":"2020-04-01"
        }
    ]

I want to pull the data from that nested dictionary but I am not sure how.
This is my attempt which hasn't worked but just wanted to show the data:

import requests
import json

def get_covid_data_19(country_code, date):
    '''
    This function gets COVID 19 data via rapid API. This takes two parameters:
    Country Name (string) and Date (YYYY-MM-DD)

    '''
    c = country_code
    d = date
    # Endpoint that we are going to be hitting:
    url = "https://covid-19-data.p.rapidapi.com/report/country/code"

    querystring = {"code" : c, "date" : d}

    headers = {
        'x-rapidapi-host': "covid-19-data.p.rapidapi.com",
        'x-rapidapi-key': "insertkeyhere"
        }

    response = requests.request("GET", url, headers=headers, params=querystring)
    json_data = response.json()

    data = []
    for response in json_data:
        data.append ({
            'confirmed' : response.get('confirmed'),
            'recovered' : response.get('recovered'),
            'deaths' : response.get('deaths'),
            'active' : response.get('active'),
            'date' : response.get('date'),
        })

    print(data)

I also attemped to retrieve the data as xml format and could not figure out how to expand the array either.
Any advice would be awesome. Thanks.


问:
convert a list of numpy arrays into a numpy array in which elements are numpy arrays
Python code to convert a list of numpy arrays into a numpy array having numpy arrays as its elements?

I have a list of arrays as below:

```<code>my_list
</code>```

Output:

```<code>[array([1,2,3]),
    array(['a', 'b','c]),
    array(['text1', 'text2', 'text3'])
    ]
</code>```

using np.asarray() converts the elements to list like the one shown below:

```<code>my_array = np.asarray(my_list)
</code>```

my_array looks like:

```<code>array([[1,2,3],['a', 'b','c], ['text1', 'text2', 'text3']])
</code>```

The kind of output that I want:

```<code>array([array([1,2,3]),array(['a', 'b','c]),array(['text1', 'text2', 'text3'])])
</code>```

Can someone please help me with the code?

答:
```<code>In [173]: alist = [np.array([1,2,3]),
     ...:     np.array(['a', 'b','c']),
     ...:     np.array(['text1', 'text2', 'text3'])
     ...:     ]
In [174]: alist
Out[174]:
[array([1, 2, 3]),
 array(['a', 'b', 'c'], dtype='<U1'),
 array(['text1', 'text2', 'text3'], dtype='<U5')]
In [175]: np.array(alist)
Out[175]:
array([['1', '2', '3'],
       ['a', 'b', 'c'],
       ['text1', 'text2', 'text3']], dtype='<U21')
</code>```

np.array tries to make as high of a multidimensional array as it can.  The 3 arrays of the same size it can make a (3,3).

To avoid that we have to create a target array of the desired shape and dtype, and assign the values from the list:

```<code>In [178]: arr = np.empty(3, object)
In [179]: arr[:] = alist
In [180]: arr
Out[180]:
array([array([1, 2, 3]), array(['a', 'b', 'c'], dtype='<U1'),
       array(['text1', 'text2', 'text3'], dtype='<U5')], dtype=object)
</code>```

If the list contains arrays (or other objects) of a different size, np.array falls back to making an object dtype array of those items.  But pay attention to the warning, and keep in mind that this is a fallback option, and as you found, not the default behavior.

```<code>In [181]: alist.append([1,2])
In [182]: np.array(alist)
<ipython-input-182-7512d762195a>:1: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  np.array(alist)
Out[182]:
array([array([1, 2, 3]), array(['a', 'b', 'c'], dtype='<U1'),
       array(['text1', 'text2', 'text3'], dtype='<U5'), list([1, 2])],
      dtype=object)
</code>```

And to repeat my comment - why do you want an array of arrays?  What advantage does that have compared to the original list?

Body:
Python code to convert a list of numpy arrays into a numpy array having numpy arrays as its elements?

I have a list of arrays as below:

my_list

Output:

[array([1,2,3]),
    array(['a', 'b','c]),
    array(['text1', 'text2', 'text3'])
    ]

using np.asarray() converts the elements to list like the one shown below:

my_array = np.asarray(my_list)

my_array looks like:

array([[1,2,3],['a', 'b','c], ['text1', 'text2', 'text3']])

The kind of output that I want:

array([array([1,2,3]),array(['a', 'b','c]),array(['text1', 'text2', 'text3'])])

Can someone please help me with the code?

Body:
In [173]: alist = [np.array([1,2,3]),
     ...:     np.array(['a', 'b','c']),
     ...:     np.array(['text1', 'text2', 'text3'])
     ...:     ]
In [174]: alist
Out[174]:
[array([1, 2, 3]),
 array(['a', 'b', 'c'], dtype='<U1'),
 array(['text1', 'text2', 'text3'], dtype='<U5')]
In [175]: np.array(alist)
Out[175]:
array([['1', '2', '3'],
       ['a', 'b', 'c'],
       ['text1', 'text2', 'text3']], dtype='<U21')

np.array tries to make as high of a multidimensional array as it can.  The 3 arrays of the same size it can make a (3,3).

To avoid that we have to create a target array of the desired shape and dtype, and assign the values from the list:

In [178]: arr = np.empty(3, object)
In [179]: arr[:] = alist
In [180]: arr
Out[180]:
array([array([1, 2, 3]), array(['a', 'b', 'c'], dtype='<U1'),
       array(['text1', 'text2', 'text3'], dtype='<U5')], dtype=object)

If the list contains arrays (or other objects) of a different size, np.array falls back to making an object dtype array of those items.  But pay attention to the warning, and keep in mind that this is a fallback option, and as you found, not the default behavior.

In [181]: alist.append([1,2])
In [182]: np.array(alist)
<ipython-input-182-7512d762195a>:1: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  np.array(alist)
Out[182]:
array([array([1, 2, 3]), array(['a', 'b', 'c'], dtype='<U1'),
       array(['text1', 'text2', 'text3'], dtype='<U5'), list([1, 2])],
      dtype=object)

And to repeat my comment - why do you want an array of arrays?  What advantage does that have compared to the original list?


问:
Writing a pandas DataFrame to CSV file
I have a dataframe in pandas which I would like to write to a CSV file.

I am doing this using:

df.to_csv('out.csv')
```
<p>And getting the following error:</p>
<pre class="lang-none prettyprint-override"><code>UnicodeEncodeError: 'ascii' codec can't encode character u'\u03b1' in position 20: ordinal not in range(128)
</code>```

Is there any way to get around this easily (i.e. I have unicode characters in my data frame)?

And is there a way to write to a tab delimited file instead of a CSV using e.g. a 'to-tab' method (that I don't think exists)?

答:
I would avoid using the '\t' separate and would create issues when reading the dataset again.

df.to_csv(file_name, encoding='utf-8')

Title: Writing a pandas DataFrame to CSV file

Body:
I have a dataframe in pandas which I would like to write to a CSV file.

I am doing this using:

df.to_csv('out.csv')

And getting the following error:

UnicodeEncodeError: 'ascii' codec can't encode character u'\u03b1' in position 20: ordinal not in range(128)

Is there any way to get around this easily (i.e. I have unicode characters in my data frame)?

And is there a way to write to a tab delimited file instead of a CSV using e.g. a 'to-tab' method (that I don't think exists)?


问:
Update last row of Google sheet with Gmail attachment
The below code is working perfectly well, but rather than replacing the entire sheet, I would like to keep pre-existing rows and add the data into new rows.

Below is as close as I have gotten.

Thank you for your help

```<code>function myFunction() {
  var thread = GmailApp.getUserLabelByName("mkto").getThreads(0,1);
  var messages = thread[0].getMessages();
  var len = messages.length;
  var message = messages[len-1]; //get last message
  var attachments = message.getAttachments(); // Get attachment of first message
  var blob = attachments[0]; // Is supposes that attachments[0] is the blob of xlsx file.
  blob.setContentTypeFromExtension();

  if (blob.getContentType() == MimeType.MICROSOFT_EXCEL) {

    // Process for XLSX
    var convertedSpreadsheetId = Drive.Files.insert({mimeType: MimeType.GOOGLE_SHEETS}, blob).id;
    var sheet = SpreadsheetApp.openById(convertedSpreadsheetId).getSheets()[0]; // There is the data in 1st tab.
    var data = sheet.getDataRange().getValues();
    Drive.Files.remove(convertedSpreadsheetId); // Remove the converted file.
    var sheet_new = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    sheet_new.clearContents();
    var range = sheet_new.getRange(1,1, data.length,data[0].length);
    range.setValues(data);

  } else if (blob.getContentType() == MimeType.CSV) {

    // Process for CSV
    var csv =  blob.getDataAsString();
    var data = Utilities.parseCsv(csv);
    var a = data.length ;
    var b = data[0].length;
    var sheet = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    sheet.getRange("A:J").clear();
    var range_final = sheet.getRange(1,1, data.length,data[0].length);
    range_final.setValues(data);

  }
</code>```

答:
comment out the sheet.clear function to keep the value and append the values from last row +1</p>
```<code>function myFunction() {
  var thread = GmailApp.getUserLabelByName("mkto").getThreads(0,1);
  var messages = thread[0].getMessages();
  var len = messages.length;
  var message = messages[len-1]; //get last message
  var attachments = message.getAttachments(); // Get attachment of first message
  var blob = attachments[0]; // Is supposes that attachments[0] is the blob of xlsx file.
  blob.setContentTypeFromExtension();

  if (blob.getContentType() == MimeType.MICROSOFT_EXCEL) {

    // Process for XLSX
    var convertedSpreadsheetId = Drive.Files.insert({mimeType: MimeType.GOOGLE_SHEETS}, blob).id;
    var sheet = SpreadsheetApp.openById(convertedSpreadsheetId).getSheets()[0]; // There is the data in 1st tab.
    var data = sheet.getDataRange().getValues();
    Drive.Files.remove(convertedSpreadsheetId); // Remove the converted file.
    var sheet_new = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    //sheet_new.clearContents();
    var range = sheet_new.getRange(sheet_new.getLastRow()+1, 1, data.length,data[0].length);
    range.setValues(data);

  } else if (blob.getContentType() == MimeType.CSV) {

    // Process for CSV
    var csv =  blob.getDataAsString();
    var data = Utilities.parseCsv(csv);
    var a = data.length ;
    var b = data[0].length;
    var sheet = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    //sheet.getRange("A:J").clear();
    var range = sheet.getRange(sheet.getLastRow()+1, 1, data.length,data[0].length);
    range.setValues(data);

  }
}
</code>```

Body:
The below code is working perfectly well, but rather than replacing the entire sheet, I would like to keep pre-existing rows and add the data into new rows.

Below is as close as I have gotten.

Thank you for your help

function myFunction() {
  var thread = GmailApp.getUserLabelByName("mkto").getThreads(0,1);
  var messages = thread[0].getMessages();
  var len = messages.length;
  var message = messages[len-1]; //get last message
  var attachments = message.getAttachments(); // Get attachment of first message
  var blob = attachments[0]; // Is supposes that attachments[0] is the blob of xlsx file.
  blob.setContentTypeFromExtension();

  if (blob.getContentType() == MimeType.MICROSOFT_EXCEL) {

    // Process for XLSX
    var convertedSpreadsheetId = Drive.Files.insert({mimeType: MimeType.GOOGLE_SHEETS}, blob).id;
    var sheet = SpreadsheetApp.openById(convertedSpreadsheetId).getSheets()[0]; // There is the data in 1st tab.
    var data = sheet.getDataRange().getValues();
    Drive.Files.remove(convertedSpreadsheetId); // Remove the converted file.
    var sheet_new = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    sheet_new.clearContents();
    var range = sheet_new.getRange(1,1, data.length,data[0].length);
    range.setValues(data);

  } else if (blob.getContentType() == MimeType.CSV) {

    // Process for CSV
    var csv =  blob.getDataAsString();
    var data = Utilities.parseCsv(csv);
    var a = data.length ;
    var b = data[0].length;
    var sheet = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    sheet.getRange("A:J").clear();
    var range_final = sheet.getRange(1,1, data.length,data[0].length);
    range_final.setValues(data);

  }

Body:
comment out the sheet.clear function to keep the value and append the values from last row +1

function myFunction() {
  var thread = GmailApp.getUserLabelByName("mkto").getThreads(0,1);
  var messages = thread[0].getMessages();
  var len = messages.length;
  var message = messages[len-1]; //get last message
  var attachments = message.getAttachments(); // Get attachment of first message
  var blob = attachments[0]; // Is supposes that attachments[0] is the blob of xlsx file.
  blob.setContentTypeFromExtension();

  if (blob.getContentType() == MimeType.MICROSOFT_EXCEL) {

    // Process for XLSX
    var convertedSpreadsheetId = Drive.Files.insert({mimeType: MimeType.GOOGLE_SHEETS}, blob).id;
    var sheet = SpreadsheetApp.openById(convertedSpreadsheetId).getSheets()[0]; // There is the data in 1st tab.
    var data = sheet.getDataRange().getValues();
    Drive.Files.remove(convertedSpreadsheetId); // Remove the converted file.
    var sheet_new = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    //sheet_new.clearContents();
    var range = sheet_new.getRange(sheet_new.getLastRow()+1, 1, data.length,data[0].length);
    range.setValues(data);

  } else if (blob.getContentType() == MimeType.CSV) {

    // Process for CSV
    var csv =  blob.getDataAsString();
    var data = Utilities.parseCsv(csv);
    var a = data.length ;
    var b = data[0].length;
    var sheet = SpreadsheetApp.openById("1Bmcl6p42rBHeIswaSWV861bCrlhZlAJtHPqwJYsnClc").getSheetByName("data");
    //sheet.getRange("A:J").clear();
    var range = sheet.getRange(sheet.getLastRow()+1, 1, data.length,data[0].length);
    range.setValues(data);

  }
}


问:
Unable to get raw TCP traffic through Istio Gateway
I can route HTTP traffic (e.g. Elasticsearch and various dashboards) through Istio Gateway, but I can't get raw TCP traffic through. I have two examples below (postgres and redpanda). I have no trouble accessing the underlying services (mypostgres.default.svc.cluster.local and three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local) internally with postgres and kafka clients.

My Gateway:

```<code>apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - 'mydomain.cloud'
    tls:
      httpsRedirect: true
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
      - 'mydomain.cloud'
    tls:
      mode: SIMPLE
      credentialName: letsencrypt-staging-tls
  - port:
      number: 9092
      name: redpanda
      protocol: TCP
    hosts:
    - 'mydomain.cloud'
  - port:
      number: 5432
      name: postgres
      protocol: TCP
    hosts:
    - 'mydomain.cloud'
</code>```

Postgres spec:

```<code>apiVersion: kubegres.reactive-tech.io/v1
kind: Kubegres
metadata:
  name: mypostgres
  namespace: postgres

spec:

   replicas: 3
   image: postgres:13.2

   database:
      size: 50Gi
      storageClassName: postgres

   env:
      - name: POSTGRES_PASSWORD
        valueFrom:
           secretKeyRef:
              name: postgressecret
              key: superUserPassword

      - name: POSTGRES_REPLICATION_PASSWORD
        valueFrom:
           secretKeyRef:
              name: postgressecret
              key: replicationUserPassword
</code>```

Virtual service:

```<code>spec:
  hosts:
  - "*"
  gateways:
  - istio-system/gateway
  tcp:
  - match:
    - port: 5432
    route:
    - destination:
        host: mypostgres.default.svc.cluster.local
        port:
          number: 5432
</code>```

Destination rule

```<code>apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: postgres-destination-rule
  namespace: default
spec:
  host: mypostgres.default.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE
</code>```

Redpanda

```<code>apiVersion: redpanda.vectorized.io/v1alpha1
kind: Cluster
metadata:
  name: three-node-cluster
spec:
  image: "vectorized/redpanda"
  version: "latest"
  replicas: 2
  resources:
    requests:
      cpu: 1
      memory: 2Gi
    limits:
      cpu: 1
      memory: 2Gi
  configuration:
    rpcServer:
      port: 33145
    kafkaApi:
    - port: 9092
    pandaproxyApi:
    - port: 8082
    adminApi:
    - port: 9644
    developerMode: true
  storage:
    storageClassName: redpanda
</code>```

Virtual service

```<code>apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: redpanda-vts
  namespace: redpanda-system
spec:
  hosts:
  - "*"
  gateways:
  - istio-system/gateway
  tcp:
  - match:
    - port: 9092
    route:
    - destination:
        host: three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local
        port:
          number: 9092
</code>```

Destination rule:

```<code>apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: redpanda-destination-rule
  namespace: redpanda-system
spec:
  host: three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE
</code>```

Any ideas? I've tried playing around with the host names, using asterisks instead of domain names, but no effect. Getting TLS will be another day's fight, but now I'd just like to get some traffic through.

For example, the following works for RedPanda from inside the cluster with the standard kafka-python client:

```<code>from kafka.admin import KafkaAdminClient, NewTopic

nodes = {'bootstrap.servers':'three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local, three-node-cluster-1.three-node-cluster.redpanda-system.svc.cluster.local'}

admin_client = KafkaAdminClient(
    bootstrap_servers=nodes['bootstrap.servers'],
    client_id='test'
)

topic_list = []
topic_list.append(NewTopic(name="test-topic", num_partitions=1, replication_factor=1))
admin_client.create_topics(new_topics=topic_list, validate_only=False)

</code>```

Similarly, I would like to be able to do the following from outside K8s through Istio Gateway:

```<code>from kafka.admin import KafkaAdminClient, NewTopic

nodes = {'bootstrap.servers':'mydomain.cloud/kafka'}

admin_client = KafkaAdminClient(
    bootstrap_servers=nodes['bootstrap.servers'],
    client_id='test'
)

topic_list = []
topic_list.append(NewTopic(name="test-topic", num_partitions=1, replication_factor=1))
admin_client.create_topics(new_topics=topic_list, validate_only=False)
</code>```

答:
Based on the documentation about Istio Protocol Selection

Istio supports proxying any TCP traffic. This includes HTTP, HTTPS, gRPC, as well as raw TCP protocols. In order to provide additional capabilities, such as routing and rich metrics, the protocol must be determined. This can be done automatically or explicitly specified.

And the answer to your problem should be in this fragment:

Protocols can be specified manually in the Service definition.

This can be configured in two ways:

By the name of the port:  name: <protocol>[-<suffix>].

In Kubernetes 1.18+, by the  appProtocol  field:  appProtocol: <protocol>.

Note that behavior at the Gateway differs in some cases as the gateway can terminate TLS and the protocol can be negotiated.

Look at the example yaml:

Below is an example of a Service that defines a https port by appProtocol and an http port by name:

```<code>kind: Service
metadata:
  name: myservice
spec:
  ports:
  - number: 3306
    name: database
    appProtocol: https <-change here 'https' to 'tcp'
  - number: 80
    name: http-web
</code>```

In your situation try to replace appProtocol: https and put appProtocol: tcp in your Service yaml

Bare in mind that Server First protocols, such as MySQL, are incompatible with automatic protocol selection. See Server first protocols for more information.

Body:
I can route HTTP traffic (e.g. Elasticsearch and various dashboards) through Istio Gateway, but I can't get raw TCP traffic through. I have two examples below (postgres and redpanda). I have no trouble accessing the underlying services (mypostgres.default.svc.cluster.local and three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local) internally with postgres and kafka clients.

My Gateway:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - 'mydomain.cloud'
    tls:
      httpsRedirect: true
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
      - 'mydomain.cloud'
    tls:
      mode: SIMPLE
      credentialName: letsencrypt-staging-tls
  - port:
      number: 9092
      name: redpanda
      protocol: TCP
    hosts:
    - 'mydomain.cloud'
  - port:
      number: 5432
      name: postgres
      protocol: TCP
    hosts:
    - 'mydomain.cloud'

Postgres spec:

apiVersion: kubegres.reactive-tech.io/v1
kind: Kubegres
metadata:
  name: mypostgres
  namespace: postgres

spec:

   replicas: 3
   image: postgres:13.2

   database:
      size: 50Gi
      storageClassName: postgres

   env:
      - name: POSTGRES_PASSWORD
        valueFrom:
           secretKeyRef:
              name: postgressecret
              key: superUserPassword

      - name: POSTGRES_REPLICATION_PASSWORD
        valueFrom:
           secretKeyRef:
              name: postgressecret
              key: replicationUserPassword

Virtual service:

spec:
  hosts:
  - "*"
  gateways:
  - istio-system/gateway
  tcp:
  - match:
    - port: 5432
    route:
    - destination:
        host: mypostgres.default.svc.cluster.local
        port:
          number: 5432

Destination rule

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: postgres-destination-rule
  namespace: default
spec:
  host: mypostgres.default.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE

Redpanda

apiVersion: redpanda.vectorized.io/v1alpha1
kind: Cluster
metadata:
  name: three-node-cluster
spec:
  image: "vectorized/redpanda"
  version: "latest"
  replicas: 2
  resources:
    requests:
      cpu: 1
      memory: 2Gi
    limits:
      cpu: 1
      memory: 2Gi
  configuration:
    rpcServer:
      port: 33145
    kafkaApi:
    - port: 9092
    pandaproxyApi:
    - port: 8082
    adminApi:
    - port: 9644
    developerMode: true
  storage:
    storageClassName: redpanda

Virtual service

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: redpanda-vts
  namespace: redpanda-system
spec:
  hosts:
  - "*"
  gateways:
  - istio-system/gateway
  tcp:
  - match:
    - port: 9092
    route:
    - destination:
        host: three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local
        port:
          number: 9092

Destination rule:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: redpanda-destination-rule
  namespace: redpanda-system
spec:
  host: three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE

Any ideas? I've tried playing around with the host names, using asterisks instead of domain names, but no effect. Getting TLS will be another day's fight, but now I'd just like to get some traffic through.

For example, the following works for RedPanda from inside the cluster with the standard kafka-python client:

from kafka.admin import KafkaAdminClient, NewTopic

nodes = {'bootstrap.servers':'three-node-cluster-0.three-node-cluster.redpanda-system.svc.cluster.local, three-node-cluster-1.three-node-cluster.redpanda-system.svc.cluster.local'}

admin_client = KafkaAdminClient(
    bootstrap_servers=nodes['bootstrap.servers'],
    client_id='test'
)

topic_list = []
topic_list.append(NewTopic(name="test-topic", num_partitions=1, replication_factor=1))
admin_client.create_topics(new_topics=topic_list, validate_only=False)

Similarly, I would like to be able to do the following from outside K8s through Istio Gateway:

from kafka.admin import KafkaAdminClient, NewTopic

nodes = {'bootstrap.servers':'mydomain.cloud/kafka'}

admin_client = KafkaAdminClient(
    bootstrap_servers=nodes['bootstrap.servers'],
    client_id='test'
)

topic_list = []
topic_list.append(NewTopic(name="test-topic", num_partitions=1, replication_factor=1))
admin_client.create_topics(new_topics=topic_list, validate_only=False)

Body:
Based on the documentation about Istio Protocol Selection

Istio supports proxying any TCP traffic. This includes HTTP, HTTPS, gRPC, as well as raw TCP protocols. In order to provide additional capabilities, such as routing and rich metrics, the protocol must be determined. This can be done automatically or explicitly specified.

And the answer to your problem should be in this fragment:

Protocols can be specified manually in the Service definition.

This can be configured in two ways:

By the name of the port:  name: <protocol>[-<suffix>].

In Kubernetes 1.18+, by the  appProtocol  field:  appProtocol: <protocol>.

Note that behavior at the Gateway differs in some cases as the gateway can terminate TLS and the protocol can be negotiated.

Look at the example yaml:

Below is an example of a Service that defines a https port by appProtocol and an http port by name:

kind: Service
metadata:
  name: myservice
spec:
  ports:
  - number: 3306
    name: database
    appProtocol: https <-change here 'https' to 'tcp'
  - number: 80
    name: http-web

In your situation try to replace appProtocol: https and put appProtocol: tcp in your Service yaml

Bare in mind that Server First protocols, such as MySQL, are incompatible with automatic protocol selection. See Server first protocols for more information.


问:
Error message "error:0308010C:digital envelope routines::unsupported"
I created the default IntelliJ IDEA React project and got this:

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:67:19)
    at Object.createHash (node:crypto:130:10)
    at module.exports (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/util/createHash.js:135:53)
    at NormalModule._initBuildHash (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:417:16)
    at handleParseError (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:471:10)
    at /Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:503:5
    at /Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:358:12
    at /Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at iterateNormalLoaders (/Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
/Users/user/Programming Documents/WebServer/untitled/node_modules/react-scripts/scripts/start.js:19
  throw err;
  ^
```

It seems to be a recent issue - webpack ran into this 4 days ago and is still working on it.

答:
In Angular.io projects, the accepted (unsupported) version of Nodes.js is v16.x.x.

In Nodes v17.x version, the same error described in this question occurs.

Solution:

Uninstall nodes and reinstall version v16.x  (for Windows).

Body:
I created the default IntelliJ IDEA React project and got this:

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:67:19)
    at Object.createHash (node:crypto:130:10)
    at module.exports (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/util/createHash.js:135:53)
    at NormalModule._initBuildHash (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:417:16)
    at handleParseError (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:471:10)
    at /Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:503:5
    at /Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:358:12
    at /Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at iterateNormalLoaders (/Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
/Users/user/Programming Documents/WebServer/untitled/node_modules/react-scripts/scripts/start.js:19
  throw err;
  ^

It seems to be a recent issue - webpack ran into this 4 days ago and is still working on it.