Azure AD B2C – Autenticação via código dotnet (C#)

12 min. leitura

Neste post você vai aprender a realizar uma autenticação no serviço do Azure AD B2C utilizando um console do DotNet Core 3.1, requisitando um access token (JWT) para assim poder acessar uma API de teste que, por sua vez, estará configurada para aceitar apenas requisições com o token de acesso.

Lembrando que esse acesso será feito apenas pelo código, sem interação com usuário. Cenário que pode ser utilizado para a comunicação de serviços background (Windows Services) por exemplo, onde seria inviável a solicitação de senha para o usuário. A vantagem desse tipo de abordagem sobre a utilização de chaves de segredo é que você possui um gerenciamento melhor sobre um usuário cadastrado do que em uma chave.

Você pode baixar o código fonte no meu GitHub pelo seguinte endereço:
https://github.com/alexandresilvadev/AutenticacaoViaCodigoAzureADB2C

Azure

A primeira coisa é configurar o Azure AD B2C, supondo que você já tenha o seu domínio criado, vamos então para o registro dos aplicativos, utilizarei dois, o primeiro é o que será utilizado pela API e o segundo é o que efetivamente será usado para o login pelo aplicativo console do C# (estou utilizando o dotnet core versão 3.1 no Windows 10).

Aplicativos no AD B2C

Entre no menu lateral na opção Registros de aplicativo e depois clique no menu superior na opção Novo registro (atente que o meu portal está configurado para o idioma português do Brasil).

Na janela de registro, entre com o nome do aplicativo (neste caso foi post-ApiWeb), selecione os tipos de conta com suporte, e no bloco de redirecionamento de URI, é necessário escolher Cliente públic/nativo e definir uma URI para o funcionamento, eu utilizei myapp://post-apiweb.

Ao clicar em registrar, você será direcionado para as propriedades do aplicativo e na tela principal você poderá visualizar os IDs que foram gerados, você irá utilizar o ID do aplicativo (cliente) e também o ID do diretório (locatário) para realizar as conexões futuramente.

Nessa mesma página de propriedades do aplicativo, vamos agora expor uma API definindo um escopo. No menu esquerdo entre em Expor uma API e depois clique no botão Adicionar um escopo.

Será aberto um frame lateral com uma URI padrão para adicionar o escopo, basta clicar no botão na parte inferior da tela escrito Salvar e continuar.

A tela será atualizada e agora mostrará os campos que serão necessários para a criação do escopo. Neste caso estou utilizando post.sample como o Nome do Escopo, é necessário também colocar um nome de exibição e uma descrição de consentimento, isso seria apresentado para o usuário caso ele precisasse consentir com o uso desse aplicativo, mas daremos esse consentimento como administrador (explicarei a frente, na criação do próximo aplicativo).

Repita novamente os passos para criar um novo aplicativo, dessa vez utilizei o nome post-ConsoleApp e a URI de redirecionamento eu selecionei Web e cadastrei a URI como https://console-app.

Clique em registrar e anote também o ID do aplicativo (cliente), o ID do locatário permanece o mesmo da primeira etapa.

Nesta mesma página, clique no menu lateral na opção Autenticação e depois habilite as opções Tokens de acesso e também Tokens de ID, para que sejam retornadas durante a autenticação.

Na parte superior da página, clique no botão Salvar para que as alterações sejam efetivadas.

Nesta mesma página, clique no menu lateral na opção Permissões de APIs e depois no botão superior Adicionar uma permissão.

O Azure irá abrir uma janela lateral com a opção de selecionar as APIs, na parte superior dessa janela, escolha a opção Minhas APIs.

Será mostrado uma lista das APIs que podem ser selecionadas, no caso, vamos selecionar a API com o nome post-ApiWeb conforme criamos anteriormente.

Vai ser exibido uma tela com as permissões possíveis para essa API, selecione as opções que deseja dar acesso e então clique em Adicionar permissões.

Ao clicar e retornar para a tela anterior você notará um alerta de que os usuários deverão consentir o acesso para essa permissão, para evitar isso, podemos (e nesse caso do acesso via código-não web, devemos) clicar no botão Conceder consentimento do administrador para <nome do aplicativo>.

Clicando no botão será mostrado uma mensagem de confirmação, basta clicar no botão Sim.

Confirmando a mensagem e retornando para a tela anterior você pode verificar que foi dado o consentimento de forma “administrativa” para os usuários.

Finalizado a criação dos aplicativos, vamos para o próximo passo.

Fluxos dos usuários

Devemos criar um fluxo de autenticação para que nosso aplicativo console consiga efetuar a autenticação sem necessidade de interação com o usuário, para isso, vamos entrar no menu Fluxos dos usuários que fica no bloco Políticas na lateral esquerda da tela.

Ao entrar nesta tela, clique em Novo fluxo de usuário.

Será mostrado uma tela para selecionar qual o tipo de fluxo que deseja criar, devemos escolher o tipo ROPC que está disponível na aba Todas. Ao entrar nessa aba, selecione então o item com o título Entrar usando ROPC.

Devemos então definir um nome para o fluxo e escolher quais as declarações deverão constar no access token a ser gerado durante a autenticação. Utilizei o nome B2C_1_Post_ROPC e selecionei algumas declarações que achei interessante para essa demonstração.

Depois de clicar em Criar, você será direcionado para uma tela com a lista de fluxos existentes, se desejar alterar alguma informação do fluxo, como tempo de vida dos tokens, basta clicar sobre o fluxo desejado que irá abrir a tela de propriedades.

Usuários

Vamos criar um usuário para a realização dos testes, clique em Usuários no menu lateral esquerdo e depois clique no botão superior Novo Usuário.

Na tela de criação do usuário existem várias opções mas nós utilizaremos a opção Criar usuário do Azure AD B2C, onde posso criar usuário sem a necessidade de um endereço de e-mail, na próxima imagem eu mostro a criação com duas formas simultâneas de login, através do Nome de Usuário, que deixei destacado em amarelo o nome postuser e também por e-mail, que utilizei o fictício postuser@domain.com destacado em verde. Para a senha, você pode criar a sua própria ou deixar o Azure gerar automaticamente, que foi a forma escolhida, para saber a senha basta marcar o check Mostrar Senha.

Pronto, terminamos o último passo dentro do painel do Azure, nós criamos os aplicativos para a API para o Console, o Fluxo de Usuários, vinculamos o acesso dos aplicativos e criamos o usuário de testes.

Codificando em C#

Agora vamos finalmente codar, vou utilizar o Visual Studio 2019 com o DotNet Core v3.1, criaremos uma solution com dois projetos, o primeiro é a API que iremos consumir e o segundo é o aplicativo console que fará a autenticação no Azure AD B2C e consumirá a API protegida.

Solution

Abrindo o Visual Studio 2019 selecione a opção Criar um projeto.

Na próxima tela, selecione Aplicativo Web ASP.NET Core.

Na configuração do novo projeto, defina o Nome do projeto como LocalWebApi e também o Nome da solução como AutenticacaoViaCodigoAzureADB2C, a pasta onde irá gravar fica a seu critério.

Projeto WebApi

A próxima etapa é definir o tipo de aplicativo Web ASP.NET Core iremos criar, como já definimos esse projeto inicial na criação da solution. Selecione a opção API e nas opções do lado direito desmarque o check da opção Configurar para HTTPS, pois realizaremos o teste apenas localmente, as opções de Autenticação não serão definidas durante a criação pois efetuaremos tudo via código.

Com o projeto LocalWebApi selecionado, mude a configuração de execução do Debug de IIS Express para LocalWebApi, assim não teremos nenhum problema com o IIS (nada contra, rsrsrs).

Para ajudar no processo, vamos utilizar o pacote NuGet próprio para o Azure AD B2C, você pode instalar ele pelo Gerenciador de pacotes do NuGet. Instale o pacote com o seguinte nome:
Microsoft.AspNetCore.Authentication.AzureADB2C.UI
A versão que estou utilizando durante esse teste é a v3.1.3

Vamos abrir o Startup.cs e configurar para que a autenticação seja validada pelo token do cliente.
As linhas destacadas (e com o asterisco no comentário) são as alterações que foram feitas no código padrão gerado pelo Visual Studio.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; // * AddAzureADB2CBearer()
using Microsoft.AspNetCore.Authentication.AzureADB2C.UI; // * AddAuthentication()
using Microsoft.AspNetCore.Authentication.JwtBearer; // * JwtBearerDefaults.AuthenticationScheme
using Microsoft.AspNetCore.Authorization; // * AuthorizationPolicyBuilder()
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace LocalWebApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // * Adiciona a autenticação
            services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
                .AddAzureADB2CBearer(options =>
                {
                    // * Instância do Azure AD B2C
                    options.Instance = "https://dominioexemplo.b2clogin.com/";
                    // * Domínio ou ID do locatário do Azure AD B2C
                    options.Domain = "dominioexemplo.onmicrosoft.com";
                    // * Fluxo do usuário
                    options.SignUpSignInPolicyId = "B2C_1_post_ropc";
                    // * ID do aplicativo (cliente) => post-ApiWeb
                    options.ClientId = "6793d717-4bc0-4458-9bbb-df051a7fea4a";
                });

            services.AddControllers();

            // * Ativa o token como forma de autorização de acesso aos recursos
            services.AddAuthorization(
                options =>
                {
                    options.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                        .RequireAuthenticatedUser()
                        .Build());
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthentication(); // * Adiciona a autenticação
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Para o nosso exemplo, vamos utilizar a API padrão que o VS já cria, a WeatherForecast, vamos apenas fazer uma mudança no arquivo Controllers/WeatherForecastController.cs, para ativar a validação da autenticação basta acrescentar a declaração [Authorize] na action desejada, vamos fazer isso no método Get(), segue o código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; // * [Authorize]
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace LocalWebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [Authorize] // * Realiza a validação do token na chamada da action
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Pronto, a codificação da API já está pronta, se você executar o projeto agora e chamar a API deverá obter um retorno 401 – Unauthorized indicando a necessidade de passar um token válido no header da chamada (faremos isso adiante, no aplicativo console).

Projeto Console

Agora vamos criar o projeto de console, onde faremos a autenticação no Azure AD B2C de forma programática e realizaremos a chamada da API passando o access token para liberação do acesso.

Para criar o novo projeto, clique com o botão direito do mouse sobre o nome da solução no Gerenciador de Soluções do Visual Studio, escolha Adicionar e depois Novo Projeto.

Escolha Aplicativo do Console (.NET Core) e clique em Próximo.

Defina o nome como AplicativoConsole e clique em Criar.

Com o projeto criado, vamos configurar ele como o projeto de inicialização, para fazermos os primeiros testes com a autenticação. Para isso, clique com o botão direito em cima do nome do projeto e escolha a opção Definir como Projeto de Inicialização.

Vamos precisar de dois pacotes NuGet, você pode instalar ele pelo Gerenciador de pacotes do NuGet.
Instale os pacotes com os seguintes nomes:
Microsoft.Identity.Client
Microsoft.IdentityModel.Clients.ActiveDirectory

Agora crie o arquivo Autenticacao.cs na raiz do projeto, ele será o responsável por efetivar a autenticação e recuperar o token de acesso para as chamadas da API protegida.

As credenciais de usuário, URIs, chaves e IDs colocados no código são apenas para exemplificação simplificada do mesmo, recomendo a utilização de outros meios para armazenamento desses valores, como por exemplo, a utilização de variáveis de ambiente.

using Microsoft.Identity.Client; // * PublicClientApplicationBuilder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security; // * SecureString()
using System.Text;
using System.Threading.Tasks;

namespace AplicativoConsole
{
    public class Autenticacao
    {
        public async Task<string> BuscarAuthorizationHeaderComUsuarioSenha()
        {
            // o nome do usuário também poderia ser
            // postuser@domain.com
            // pois cadastramos os dois
            string usuario = "postuser";
            // a senha que foi gerada ou inserida no Azure
            string senha = "Qoku7065";

            // o locatário também pode ser o ID
            // exemplo: f1a92ca1-553c-4a1d-a00c-f7ab3c1445c7
            string locatario = "dominioexemplo.onmicrosoft.com";
            // ID do aplicativo (cliente) => post-ConsoleApp
            string idCliente = "1b834a6d-04b0-43ba-9022-b0be356470f0";
            // Fluxo do usuário para a autenticaçãp
            string policySignUpSignIn = "B2C_1_post_ropc";

            // Interpolação de strings para utilização na autenticação
            string authorityBase = $"https://dominioexemplo.b2clogin.com/tfp/{locatario}/";
            string authority = $"{authorityBase}{policySignUpSignIn}";

            // O escopo é referente a API exposta no aplicativo post-ApiWeb
            // pois é quem efetivamente iremos acessar
            string[] scopes = new string[]
            {
                // aqui é utilizado o ID do aplicativo (cliente) => post-ApiWeb
                "https://dominioexemplo.onmicrosoft.com/6793d717-4bc0-4458-9bbb-df051a7fea4a/post.sample"
            };

            var application = PublicClientApplicationBuilder.Create(idCliente)
                .WithB2CAuthority(authority)
                .Build();
            IEnumerable<IAccount> accounts = await application.GetAccountsAsync();

            string authorizationHeader = string.Empty;
            AuthenticationResult authenticationResult = null;
            if (accounts.Any())
            {
                authenticationResult =
                    await application.AcquireTokenSilent(scopes,
                                                         accounts.FirstOrDefault())
                    .ExecuteAsync();
            }
            else
            {
                try
                {
                    // cria uma forma segura de transmitir a senha
                    // com o objeto do tipo SecureString
                    var senhaSegura = new SecureString();
                    foreach (char c in senha)
                        senhaSegura.AppendChar(c);

                    // esse próximo passo eu não consegui executar com o await
                    // na execução ele parava e saia do console sem acusar 
                    // nenhum tipo de erro.
                    // por isso utilizo o .Wait() e depois pego o resultado
                    var taskAcquire = application.AcquireTokenByUsernamePassword(
                        scopes,
                        usuario,
                        senhaSegura).ExecuteAsync();
                    // não determinei nenhum timeout para o .Wait()
                    taskAcquire.Wait();
                    authenticationResult = taskAcquire.Result;

                    // o comando abaixo recupera o access token 
                    // e insere o "Bearer" no início, já deixando
                    // pronto para utilização nas chamadas de API
                    authorizationHeader =
                        authenticationResult.CreateAuthorizationHeader();

                }
                catch (MsalException mex)
                {
                    // Caso ocorra algum erro na autenticação
                    // deve ser tratado aqui
                    Console.WriteLine(mex);
                }
                catch (Exception ex)
                {
                    // Qualquer outro tipo de erro
                    Console.WriteLine(ex);
                }
            }

            // dados possíveis de capturar (alguns)
            // --------------------------------------------------
            // caso queira recuperar apenas o access token
            string accessToken = authenticationResult.AccessToken;
            // caso queira recuperar apenas o ID token
            string idToken = authenticationResult.IdToken;
            // caso queira recuperar a hora em que o token expira
            DateTimeOffset expiresOn = authenticationResult.ExpiresOn;
            // --------------------------------------------------

            // retorna o header com o Bearer
            return authorizationHeader;
        }
    }
}

Feito a codificação da classe acima, vamos realizar um teste rápido, para saber se já estamos conseguir fazer a autenticação junto ao Azure AD B2C.
Para isso vamos editar o arquivo Program.cs e alterar/incluir as linhas destacadas que indicam as alterações que foram feitas no arquivo padrão gerado pelo VS.

using System;

namespace AplicativoConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Teste de autenticação no Azure AD B2C");

            string authorizationHeader =
                new Autenticacao()
                .BuscarAuthorizationHeaderComUsuarioSenha().Result;

            Console.WriteLine("==================================");
            Console.WriteLine("Header retornado pela autenticação");
            Console.WriteLine("==================================");
            
            Console.WriteLine(authorizationHeader);
            
            Console.WriteLine("==================================");
            Console.WriteLine("[--- FIM ---]");
        }
    }
}

Agora já podemos fazer um teste de autenticação, execute o seu projeto de console, ele vai mostrar a primeira mensagem, demorar alguns segundos (dependendo da sua internet) e na sequência irá apresentar o resultado, que é o conteúdo do header a ser enviado nas chamadas da API, veja o resultado abaixo:

Você pode verificar que ele adicionou a palavra Bearer antes do token, e por motivos de segurança eu risquei parte do conteúdo do meu token de teste.
Você pode copiar esse conteúdo do token (sem a palavra Bearer) e abrir o conteúdo dele no site jwt.io onde você obterá um resultado parecido com a imagem abaixo:

Feito o teste, o console conseguiu autenticar sem erro, vamos agora criar o arquivo WebApi.cs na raiz do projeto. Ele será responsável por realizar a chamada da nossa API protegida.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http; // * HttpClient
using System.Net.Http.Headers; // * MediaTypeWithQualityHeaderValue()
using System.Text;
using System.Threading.Tasks;

namespace AplicativoConsole
{
    public class WebApi
    {
        private static readonly HttpClient httpClient = new HttpClient();

        public static void ChamarWebApi(string AuthorizationHeader)
        {
            // a URI do nosso projeto está a padrão, na porta 5000
            httpClient.BaseAddress = new Uri("http://localhost:5000/");

            // definimos o header para aceitar um json como reposta
            httpClient.DefaultRequestHeaders.Accept.Clear();
            httpClient.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));

            // aqui definimos o Header de requisição com
            // o token retornado do passo anterior
            // (será passado como parâmetro neste método)
            httpClient.DefaultRequestHeaders
                .Add("Authorization", AuthorizationHeader);

            // Faremos uma requisição da API que está em execução
            // A action acionada será a WeatherForecast
            HttpResponseMessage response =
                httpClient.GetAsync("WeatherForecast/")
                .Result;

            Console.WriteLine($"Resultado da chamada: {response.StatusCode}");

            // Verifica se foi uma resposta de sucesso da API
            if (response.IsSuccessStatusCode)
            {
                // Imprime o conteúdo da resposta
                var conteudo = response.Content.ReadAsStringAsync().Result;
                Console.WriteLine("==================================");
                Console.WriteLine(conteudo);
                Console.WriteLine("==================================");
            }
        }
    }
}

Agora vamos voltar no arquivo Program.cs e realizar as alterações destacadas no código abaixo:

using System;

namespace AplicativoConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Teste de autenticação no Azure AD B2C");

            string authorizationHeader =
                new Autenticacao()
                .BuscarAuthorizationHeaderComUsuarioSenha().Result;

            Console.WriteLine("==================================");
            Console.WriteLine("Header retornado pela autenticação");
            Console.WriteLine("==================================");
            
            Console.WriteLine(authorizationHeader);
            
            Console.WriteLine("==================================");

            Console.WriteLine("\n\n[--- Aperte ENTER para chamar a API ---]");
            Console.ReadLine();

            // Realiza a chamada da API
            WebApi.ChamarWebApi(authorizationHeader);

            Console.WriteLine("[--- FIM ---]");

            // Aguarda um ENTER para sair do console
            Console.ReadLine();
        }
    }
}

Próximo passo é configurar a nossa solução para executar os dois projetos ao mesmo tempo, a API e o console, para isso, clique com o botão direito do mouse em cima do nome da solução e escolha a opção Definir Projetos de Inicialização.

Na tela que se abre, escolha o check box Vários projetos de inicialização, coloque a opção Iniciar na coluna Ação para os dois projetos.
Para ficar mais prático, deixe os projetos na sequência: LocalWebApi e AplicativoConsole, você pode usar as setas na lateral direita para mudar as posições. Feito as alterações, clique no botão ok.

Agora execute a solução (apertando F5 ou clicando em Iniciar), serão carregadas duas telas de console, uma referente ao servidor da web api e outra o nosso console.
Em alguns instantes o console já irá apresentar o token de acesso e a mensagem para teclar ENTER para continuar, quando você teclar ENTER, o console fará uma requisição ao servidor da Web Api passando o token de acesso, feito isso, o resultado deverá ser OK e impresso também o resultado da consulta, que é uma string Json com os dados aleatórios gerados pela API, conforme mostra a imagem abaixo.

Chegamos ao fim desta solução e como eu disse no início, foi um teste rápido para solucionar o meu problema, você tem inúmeras coisas que pode fazer em cima disso, como tratar o tempo de vida do token para solicitar um novo quando expirar, capturar os dados do usuário que efetuou o login (nome, endereço, etc) e muito mais.


Bom, espero realmente que você tenha compreendido e realizado com sucesso o teste deste post, se tiver alguma dúvida ou sugestão, por favor, pode colocar nos comentários.

Deixe uma resposta