programing

Azure Active Directory B2C에서 그룹별 인증

stoneblock 2023. 5. 19. 23:56

Azure Active Directory B2C에서 그룹별 인증

Azure Active Directory B2C에서 그룹을 사용하여 권한을 부여하는 방법을 찾고 있습니다.사용자를 통해 인증할 수 있습니다. 예:

[Authorize(Users="Bill")]

그러나 이 방법은 그다지 효과적이지 않으며 이에 대한 사용 사례는 거의 없습니다.대체 솔루션은 역할을 통해 권한 부여입니다.하지만 어떤 이유에서인지 그것은 효과가 없는 것 같습니다.예를 들어 사용자에게 "Global Admin" 역할을 지정하고 다음을 시도하면 작동하지 않습니다.

[Authorize(Roles="Global Admin")]

그룹 또는 역할을 통해 권한을 부여하는 방법이 있습니까?

Azure AD에서 사용자를 위한 그룹 멤버십을 얻는 데는 "코드 몇 줄" 이상의 많은 것이 필요합니다. 그래서 저는 며칠 동안 머리를 쥐어뜯고 머리를 쥐어뜯는 것을 다른 사람들을 구하기 위해 마침내 제게 효과가 있었던 것을 공유하려고 생각했습니다.

먼저 project.json에 다음 종속성을 추가합니다.

"dependencies": {
    ...
    "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.8",
    "Microsoft.Azure.ActiveDirectory.GraphClient": "2.0.2"
}

우리의 애플리케이션이 AAD Graph API에 접근하기 위해서는 인증이 필요하기 때문에 첫 번째 것이 필요합니다.두 번째는 Graph API 클라이언트 라이브러리로 사용자 멤버십을 쿼리하는 데 사용할 것입니다.이 버전은 이 문서 작성 시점에서만 유효하며 향후 변경될 수 있음은 두말할 나위도 없습니다.

다음으로 Startup 클래스의 Configure() 메서드에서 Open을 구성하기 직전일 수 있습니다.ID Connect 인증은 다음과 같이 Graph API 클라이언트를 만듭니다.

var authContext = new AuthenticationContext("https://login.microsoftonline.com/<your_directory_name>.onmicrosoft.com");
var clientCredential = new ClientCredential("<your_b2c_app_id>", "<your_b2c_secret_app_key>");
const string AAD_GRAPH_URI = "https://graph.windows.net";
var graphUri = new Uri(AAD_GRAPH_URI);
var serviceRoot = new Uri(graphUri, "<your_directory_name>.onmicrosoft.com");
this.aadClient = new ActiveDirectoryClient(serviceRoot, async () => await AcquireGraphAPIAccessToken(AAD_GRAPH_URI, authContext, clientCredential));

경고: 비밀 앱 키를 하드 코딩하지 말고 안전한 장소에 보관하십시오.음, 당신은 이미 알고 계셨죠?:)

비동기 획득 그래프APIAccessAD 클라이언트 생성자에게 전달한 토큰() 메서드는 클라이언트가 인증 토큰을 얻어야 할 때 필요에 따라 호출됩니다.메소드는 다음과 같습니다.

private async Task<string> AcquireGraphAPIAccessToken(string graphAPIUrl, AuthenticationContext authContext, ClientCredential clientCredential)
{
    AuthenticationResult result = null;
    var retryCount = 0;
    var retry = false;

    do
    {
        retry = false;
        try
        {
            // ADAL includes an in-memory cache, so this will only send a request if the cached token has expired
            result = await authContext.AcquireTokenAsync(graphAPIUrl, clientCredential);
        }
        catch (AdalException ex)
        {
            if (ex.ErrorCode == "temporarily_unavailable")
            {
                retry = true;
                retryCount++;
                await Task.Delay(3000);
            }
        }
    } while (retry && (retryCount < 3));

    if (result != null)
    {
        return result.AccessToken;
    }

    return null;
}

응용 프로그램의 필요에 따라 조정할 수 있는 임시 조건을 처리하기 위한 기본 재시도 메커니즘이 있습니다.

이제 애플리케이션 인증 및 AD 클라이언트 설정을 완료했으므로 OpenIdConnect 이벤트를 활용하여 최종적으로 활용할 수 있습니다.Configure Configure()라고 합니다.()라고 합니다.app.UseOpenIdConnectAuthentication()OpenIdConnectOptions 인스턴스를 생성하고 OnTokenValidated 이벤트에 대한 이벤트 핸들러를 추가합니다.

new OpenIdConnectOptions()
{
    ...         
    Events = new OpenIdConnectEvents()
    {
        ...
        OnTokenValidated = SecurityTokenValidated
    },
};

로그인 사용자에 대한 액세스 토큰을 획득, 검증 및 사용자 ID가 설정된 경우 이벤트가 발생합니다. (AAD Graph API 호출에 필요한 애플리케이션 자체 액세스 토큰과 혼동하지 마십시오!)Graph API를 쿼리하여 사용자의 그룹 멤버십을 조회하고 추가 클레임 형식으로 해당 그룹을 ID에 추가하기에 적합한 것으로 보입니다.

private Task SecurityTokenValidated(TokenValidatedContext context)
{
    return Task.Run(async () =>
    {
        var oidClaim = context.SecurityToken.Claims.FirstOrDefault(c => c.Type == "oid");
        if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
        {
            var pagedCollection = await this.aadClient.Users.GetByObjectId(oidClaim.Value).MemberOf.ExecuteAsync();

            do
            {
                var directoryObjects = pagedCollection.CurrentPage.ToList();
                foreach (var directoryObject in directoryObjects)
                {
                    var group = directoryObject as Group;
                    if (group != null)
                    {
                        ((ClaimsIdentity)context.Ticket.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String));
                    }
                }
                pagedCollection = pagedCollection.MorePagesAvailable ? await pagedCollection.GetNextPageAsync() : null;
            }
            while (pagedCollection != null);
        }
    });
}

여기서 사용되는 것은 역할 클레임 유형이지만 사용자 지정 클레임 유형을 사용할 수 있습니다.

ClaimType을 사용하는 경우 위의 작업을 수행합니다.역할, 컨트롤러 클래스 또는 메서드를 다음과 같이 꾸미기만 하면 됩니다.

[Authorize(Role = "Administrators")]

B2C에서 "Administrators"라는 표시 이름으로 지정된 그룹이 구성되어 있는 경우에도 마찬가지입니다.

그러나 사용자 지정 클레임 유형을 사용하도록 선택한 경우 서비스 구성() 방법에서 다음과 같은 방법을 추가하여 클레임 유형을 기반으로 권한 부여 정책을 정의해야 합니다.

services.AddAuthorization(options => options.AddPolicy("ADMIN_ONLY", policy => policy.RequireClaim("<your_custom_claim_type>", "Administrators")));

권한 있는 컨트롤러 클래스 또는 메서드를 다음과 같이 장식합니다.

[Authorize(Policy = "ADMIN_ONLY")]

좋아요, 아직 안 끝났나요? - 글쎄요, 정확히는 아닙니다.

응용 프로그램을 실행하고 로그인하려고 하면 Graph API에서 "권한이 부족하여 작업을 완료할 수 없습니다."라는 예외가 발생합니다.분명하지 않을 수도 있지만 애플리케이션이 app_id 및 app_key를 사용하여 AD를 성공적으로 인증하는 동안에는 AD에서 사용자의 세부 정보를 읽는 데 필요한 권한이 없습니다.애플리케이션에 이러한 액세스 권한을 부여하기 위해 PowerShell용 Azure Active Directory 모듈을 사용하기로 선택했습니다.

다음 스크립트가 저에게 도움이 되었습니다.

$tenantGuid = "<your_tenant_GUID>"
$appID = "<your_app_id>"

$userVal = "<admin_user>@<your_AD>.onmicrosoft.com"
$pass = "<admin password in clear text>"
$Creds = New-Object System.Management.Automation.PsCredential($userVal, (ConvertTo-SecureString $pass -AsPlainText -Force))

Connect-MSOLSERVICE -Credential $Creds
$msSP = Get-MsolServicePrincipal -AppPrincipalId $appID -TenantID $tenantGuid

$objectId = $msSP.ObjectId

Add-MsolRoleMember -RoleName "Company Administrator" -RoleMemberType ServicePrincipal -RoleMemberObjectId $objectId

드디어 끝이 났습니다!"코드 몇 줄"은 어때요?:)

이렇게 하면 작동하지만, 원하는 것을 달성하려면 인증 로직에 코드를 몇 줄 작성해야 합니다.

은 우선, 당은그구합니다야별해것을신ish합▁between다니를 구별해야 합니다.Roles그리고.GroupsAzure AD(B2C)는 다음과 같습니다.

User Role매우 구체적이며 Azure AD(B2C) 자체 내에서만 유효합니다.역할은 사용자가 Azure AD 내에서 가지는 권한을 정의합니다.

Group(또는)Security Group)는 외부 응용 프로그램에 노출될 수 있는 사용자 그룹 구성원 자격을 정의합니다.외부 응용프로그램은 Security Group 역할 기반 액세스 제어를 모델링할 수 있습니다.네, 좀 헷갈릴 수도 있겠지만, 그게 사실입니다.

첫 번째 단계는 모델을 만드는 것입니다.GroupsAzure AD B2C - 그룹을 만들고 해당 그룹에 사용자를 수동으로 할당해야 합니다.Azure Portal(https://portal.azure.com/) :

푸른색 포털의 그림

그런 다음 사용자가 성공적으로 인증되면 응용 프로그램으로 돌아가서 약간의 코드를 입력하고 Azure AD B2C Graph API에 사용자 멤버십을 요청해야 합니다. 샘플을 사용하여 사용자 그룹 구성원 자격을 얻는 방법에 대한 영감을 얻을 수 있습니다.이 코드는 열기 중 하나에서 실행하는 것이 가장 좋습니다.ID 알림(, 보안)토큰 검증됨) 및 사용자 역할을 클레임 주체에 추가합니다.

Azure AD Security Groups 및 "Role Claim" 값을 갖도록 클레임 주체를 변경하면 역할과 함께 Authorize 특성을 사용할 수 있습니다.이것은 실제로 5-6줄의 코드입니다.

마지막으로 그래프 API를 쿼리하지 않고도 그룹 구성원 자격 클레임을 얻기 위해 여기서 기능에 대한 투표를 할 수 있습니다.

나는 이것을 서면으로 구현했지만, 2017년 5월 기준으로 라인.

((ClaimsIdentity)context.Ticket.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String));

로 변경해야 합니다.

((ClaimsIdentity)context.Ticket.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role, group.DisplayName));

최신 립과 함께 작동하도록 만드는 것.

작가님 수고하셨습니다.

또한 Connect-MsolService가 최신 lib에 잘못된 사용자 이름과 비밀번호 업데이트를 제공하는 데 문제가 있는 경우.

올바른 방향을 제시해 주셔서 감사합니다. 해결책을 찾기 위해서는 알렉스의 대답이 필수적입니다.

은 용사방을 사용합니다.app.UseOpenIdConnectAuthentication()이미 Core 2에서 평가 절하된 지 오래되었고 Core 3에서 완전히 제거되었습니다(인증 ID를 ASP.NET Core 2.0으로 마이그레이션).

해야 하는 인 작업은 를 우가구야하기작이업핸것다에 입니다.OnTokenValidated용사를 OpenIdConnectOptions후드 아래 ADB2C 인증에서 사용됩니다.우리는 ADB2C의 다른 구성을 방해하지 않고 이 작업을 수행해야 합니다.

제 의견은 이렇습니다.

// My (and probably everyone's) existing code in Startup:
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
        .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));

// This adds the custom event handler, without interfering any existing functionality:
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme,
options =>
{
    options.Events.OnTokenValidated =
        new AzureADB2CHelper(options.Events.OnTokenValidated).OnTokenValidated;
});

모든 구현은 도우미 클래스에 캡슐화되어 시작 클래스를 깨끗하게 유지합니다.null이 아닌 경우(btw가 아닌 경우) 원본 이벤트 핸들러가 저장되고 호출됩니다.

public class AzureADB2CHelper
{
    private readonly ActiveDirectoryClient _activeDirectoryClient;
    private readonly Func<TokenValidatedContext, Task> _onTokenValidated;
    private const string AadGraphUri = "https://graph.windows.net";


    public AzureADB2CHelper(Func<TokenValidatedContext, Task> onTokenValidated)
    {
        _onTokenValidated = onTokenValidated;
        _activeDirectoryClient = CreateActiveDirectoryClient();
    }

    private ActiveDirectoryClient CreateActiveDirectoryClient()
    {
        // TODO: Refactor secrets to settings
        var authContext = new AuthenticationContext("https://login.microsoftonline.com/<yourdomain, like xxx.onmicrosoft.com>");
        var clientCredential = new ClientCredential("<yourclientcredential>", @"<yourappsecret>");


        var graphUri = new Uri(AadGraphUri);
        var serviceRoot = new Uri(graphUri, "<yourdomain, like xxx.onmicrosoft.com>");
        return new ActiveDirectoryClient(serviceRoot,
            async () => await AcquireGraphAPIAccessToken(AadGraphUri, authContext, clientCredential));
    }

    private async Task<string> AcquireGraphAPIAccessToken(string graphAPIUrl,
        AuthenticationContext authContext,
        ClientCredential clientCredential)
    {
        AuthenticationResult result = null;
        var retryCount = 0;
        var retry = false;

        do
        {
            retry = false;
            try
            {
                // ADAL includes an in-memory cache, so this will only send a request if the cached token has expired
                result = await authContext.AcquireTokenAsync(graphAPIUrl, clientCredential);
            }
            catch (AdalException ex)
            {
                if (ex.ErrorCode != "temporarily_unavailable")
                {
                    continue;
                }

                retry = true;
                retryCount++;
                await Task.Delay(3000);
            }
        } while (retry && retryCount < 3);

        return result?.AccessToken;
    }

    public Task OnTokenValidated(TokenValidatedContext context)
    {
        _onTokenValidated?.Invoke(context);
        return Task.Run(async () =>
        {
            try
            {
                var oidClaim = context.SecurityToken.Claims.FirstOrDefault(c => c.Type == "oid");
                if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
                {
                    var pagedCollection = await _activeDirectoryClient.Users.GetByObjectId(oidClaim.Value).MemberOf
                        .ExecuteAsync();

                    do
                    {
                        var directoryObjects = pagedCollection.CurrentPage.ToList();
                        foreach (var directoryObject in directoryObjects)
                        {
                            if (directoryObject is Group group)
                            {
                                ((ClaimsIdentity) context.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role,
                                    group.DisplayName, ClaimValueTypes.String));
                            }
                        }

                        pagedCollection = pagedCollection.MorePagesAvailable
                            ? await pagedCollection.GetNextPageAsync()
                            : null;
                    } while (pagedCollection != null);
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }
        });
    }
}

다음과 같은 적절한 패키지가 필요합니다.

<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Microsoft.Azure.ActiveDirectory.GraphClient" Version="2.1.1" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.3" />

캐치: AD를 읽으려면 응용 프로그램에 권한을 부여해야 합니다.2019년 10월 기준으로 이 애플리케이션은 최신 B2C 애플리케이션이 아닌 '레거시' 애플리케이션이어야 합니다.여기 매우 좋은 가이드가 있습니다: Azure AD B2C: Azure AD Graph API 사용

Azure AD B2C: 역할 기반 액세스 제어는 Azure AD 팀의 공식 샘플입니다.

하지만 네, 유일한 해결책은 MS Graph의 도움을 받아 사용자 그룹을 읽는 맞춤형 구현인 것 같습니다.

여기 있는 모든 놀라운 답변을 바탕으로 새로운 Microsoft Graph API를 사용하여 사용자 그룹을 얻습니다.


IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
          .Create("application-id")
          .WithTenantId("tenant-id")
          .WithClientSecret("xxxxxxxxx")
          .Build();

ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

GraphServiceClient graphClient = new GraphServiceClient(authProvider);


var groups = await graphClient.Users[oid].MemberOf.Request().GetAsync();

저는 @AlexLobakov의 답변을 정말 좋아하지만, 저는 업데이트된 답변을 원했습니다..NET 6테스트 가능하면서도 캐싱 기능을 구현한 제품도 있습니다.또한 역할이 프런트 엔드로 전송되고 React와 같은 모든 SPA와 호환되며 애플리케이션에서 역할 기반 액세스 제어(RBAC)를 위한 표준 Azure AD B2C 사용자 흐름을 사용하기를 원했습니다.

또한 시작과 종료 가이드를 놓쳤기 때문에 오류가 발생할 수 있는 변수가 많아 애플리케이션이 작동하지 않습니다.

새기부시를 합니다.ASP.NET Core Web APIVisual Studio 2022다음 설정을 사용합니다.

생성 후 다음과 같은 대화 상자가 표시됩니다.

이 메시지가 표시되지 않으면 Visual Studio에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 Overview(개요)와 Connected services(연결된 서비스)를 클릭합니다.

새기 App registrationAzure AD B2C에서 또는 기존을 사용합니다.저는 이 데모 목적으로 새로운 것을 등록했습니다.

을 한 후App registration가 Visual Studio에서 .Dependency configuration progress나머지는 수동으로 구성됩니다.

https://portal.azure.com/, 에 로그온하고 ADB2C로 전환 디렉토리를 선택합니다.App registration[인증]을 클릭합니다.그런 다음 을 클릭합니다.Add a platform를 선택합니다.Web.

추가Redirect URI그리고.Front-channel logout URL로컬 호스트용.

예:

https://localhost:7166/signin-oidc

https://localhost:7166/logout

대신 단일 페이지 응용프로그램을 선택하면 거의 동일하게 나타납니다.그러나 아래 설명과 같이 code_challenge를 추가해야 합니다.이에 대한 전체 예는 표시되지 않습니다.

Active Directory가 PKCE를 사용한 인증 코드 흐름을 지원하지 않습니까?

인증은 다음과 같이 표시되어야 합니다.

Certificates & secrets새 클라이언트 암호를 만듭니다.

Expose an API에 편집을 해주세요.Application ID URI.

은 이와 .api://11111111-1111-1111-1111-111111111111다으로편이 되도록 합니다.https://youradb2c.onmicrosoft.com/11111111-1111-1111-1111-111111111111에 이이지정범합있라는 .access_as_user없는 경우 작성합니다.

이제클을 하십시오.API permissions:

Microsoft Graph권한이 필요합니다.

두 가지 응용 프로그램:

GroupMember.Read.All
User.Read.All

두 명의 위임:

offline_access
openid

당신은 또한 당신의 것이 필요합니다.access_as_userhttp://▁api 에서 .이 작업이 완료되면 다음을 클릭합니다.Grant admin consent for ...다음과 같이 표시해야 합니다.

없는 에는 " " " " " " " " " " 을 생성합니다.Sign up and sign in 는또.Sign in를 선택합니다.Recommended내 사용자 흐름은 기본값입니다.B2C_1_signin.

AD B2C 사용자가 인증하려는 그룹의 구성원인지 확인합니다.

여기에 이미지 설명 입력

이제 응용 프로그램으로 돌아가서 로그인할 코드를 얻을 수 있는지 확인할 수 있습니다.이 샘플을 사용하면 코드로 리디렉션됩니다.

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

문제가 해결되면 로그인 후 다음과 같은 방법으로 리디렉션해야 합니다.

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

다음과 같은 오류가 발생하는 경우:

AADB2C99059:제공된 요청은 code_challenge를 제시해야 합니다.

그러면 플랫폼을 선택했을 것입니다.Single-page application과 같은 에 code_request를 &code_challenge=123나중에 문제의 유효성을 확인해야 하기 때문에 이 방법으로는 충분하지 않습니다. 그렇지 않으면 내 코드를 실행할 때 아래 오류가 발생합니다.

AADB2C90183:제공된 code_verifier가 잘못되었습니다.

이제 응용 프로그램을 열고 다음을 수행합니다.appsettings.json기본값은 다음과 같습니다.

  "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"
  },

몇 가지 값이 더 필요하므로 결국에는 다음과 같이 표시되어야 합니다.

  "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"
  },

는 저장합니다.ClientSecret비밀 관리자입니다.

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

이제 다음 새 클래스를 만듭니다.

앱 설정:

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; }

    }
}

Adb2cToken 응답:

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; }
    }
}

캐시 키:

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

GraphApi 서비스:

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);
        }
    }
}

은 오직 재만현.accessToken위해서GraphServiceClient메모리 캐시에 저장되지만 응용 프로그램이 더 나은 성능을 필요로 하는 경우 사용자 그룹도 캐시될 수 있습니다.

새 클래스 추가:

Adb2c 사용자:

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; }
    }
}

및 구조:

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

        public const string Name = "name";
    }
}

이제 새 API 컨트롤러를 추가합니다.

로그인 컨트롤러:

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;
        }
    }
}

이제편시다니입간할집다를 편집할 입니다.Program.cs 6ASP.NET Core 6.0의 은 이와 같이 .

var builder = WebApplication.CreateBuilder(args);

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

:ASP.NET Core 6.0를 사용하고 .JwtBearerDefaults.AuthenticationScheme그리고 아닌AzureADB2CDefaults.AuthenticationScheme또는AzureADB2CDefaults.OpenIdScheme.

집편대상Program.cs다음과 같이 표시됩니다.

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();

이제 응용 프로그램을 실행하고 다음과 같은 요청에서 이전 코드를 사용할 수 있습니다.

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

"code"

그러면 다음과 같은 응답을 받게 됩니다.access_token:

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

추가하기[Authorize(Roles = "Administrator")]WeatherForecastController.cs만 이 수 할 수 .access_token우리는 더 일찍 도착했습니다.

가 로바면꾸로 ,[Authorize(Roles = "Administrator2")]동일한 사용자를 가진 HTTP 403을 얻습니다.

로그인 컨트롤러는 새로 고침 토큰도 처리할 수 있습니다.

NuGets NuGets 께와Microsoft.NET.Test.Sdk,xunit,xunit.runner.visualstudio그리고.Moq우리는 또한 테스트할 수 있습니다.LoginController로 그고차례로리로차례▁and▁in.GraphApiService에 사용되는.ClaimsIdentityProgram.cs안타깝게도 본문이 30000자로 제한되어 있기 때문에 전체 테스트를 표시할 수 없습니다.

기본적으로 다음과 같습니다.

로그인 컨트롤러 테스트:

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또한 필요하지만 그것은 단지 예시와 같은 더 많은 가치를 추가할 뿐입니다.mockMessageHandler.Protected()인 값들, 예를 들어 " 인값은다같다습니과음정적다"와 같은 것들이 .public static string DummyUserExternalId = "11111111-1111-1111-1111-111111111111";.

이를 위한 다른 방법들이 있지만 그들은 보통 의존합니다.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

먼저, 이전의 답변에 모두 감사드립니다.이 일을 처리하기 위해 하루 종일을 보냈습니다.ASPNET Core 3.1을 사용하고 있는데 이전 응답의 솔루션을 사용할 때 다음 오류가 발생했습니다.

secure binary serialization is not supported on this platform

REST API 쿼리로 대체하여 그룹을 가져올 수 있었습니다.

    public Task OnTokenValidated(TokenValidatedContext context)
    {
        _onTokenValidated?.Invoke(context);
        return Task.Run(async () =>
        {
            try
            {
                var oidClaim = context.SecurityToken.Claims.FirstOrDefault(c => c.Type == "oid");
                if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
                {
                    HttpClient http = new HttpClient();

                    var domainName = _azureADSettings.Domain;
                    var authContext = new AuthenticationContext($"https://login.microsoftonline.com/{domainName}");
                    var clientCredential = new ClientCredential(_azureADSettings.ApplicationClientId, _azureADSettings.ApplicationSecret);
                    var accessToken = AcquireGraphAPIAccessToken(AadGraphUri, authContext, clientCredential).Result;

                    var url = $"https://graph.windows.net/{domainName}/users/" + oidClaim?.Value + "/$links/memberOf?api-version=1.6";

                    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                    HttpResponseMessage response = await http.SendAsync(request);

                    dynamic json = JsonConvert.DeserializeObject<dynamic>(await response.Content.ReadAsStringAsync());

                    foreach(var group in json.value)
                    {
                        dynamic x = group.url.ToString();

                        request = new HttpRequestMessage(HttpMethod.Get, x + "?api-version=1.6");
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                        response = await http.SendAsync(request);

                        dynamic json2 = JsonConvert.DeserializeObject<dynamic>(await response.Content.ReadAsStringAsync());

                        ((ClaimsIdentity)((ClaimsIdentity)context.Principal.Identity)).AddClaim(new Claim(ClaimTypes.Role.ToString(), json2.displayName.ToString()));
                    }
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }
        });
    }

언급URL : https://stackoverflow.com/questions/40302231/authorize-by-group-in-azure-active-directory-b2c